diff --git a/README.md b/README.md index c0d44c0..333e9b9 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ These Source Generators were designed to work in full with ReactiveUI V19.5.31 a - [ReactiveCommand(CanExecute = nameof(IObservableBoolName))] with CanExecute - [ReactiveCommand][property: AttribueToAddToCommand] with Attribute passthrough - [IViewFor(nameof(ViewModelName))] +- [RoutedControlHost("YourNameSpace.CustomControl")] +- [ViewModelControlHost("YourNameSpace.CustomControl")] Versions older than V19.5.31 to this: - [ReactiveCommand] all options supported except Cancellation Token asnyc methods. @@ -84,6 +86,18 @@ public partial class MyReactiveClass : ReactiveObject } ``` +### Usage Reactive property with property Attribute pass through +```csharp +using ReactiveUI.SourceGenerators; + +public partial class MyReactiveClass : ReactiveObject +{ + [Reactive] + [property: JsonIgnore] + private string _myProperty; +} +``` + ## Usage ObservableAsPropertyHelper `[ObservableAsProperty]` ObservableAsPropertyHelper is used to create a read-only property from an IObservable. The generated code will create a backing field and a property that returns the value of the backing field. The backing field is initialized with the value of the IObservable when the class is instantiated. diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index e85fced..4222469 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -12,7 +12,7 @@ - + diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactiveProperiesWithAttributes#TestVM.Properties.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactiveProperiesWithAttributes#TestVM.Properties.g.verified.cs index 2a08ae3..3328c67 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactiveProperiesWithAttributes#TestVM.Properties.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactiveProperiesWithAttributes#TestVM.Properties.g.verified.cs @@ -7,23 +7,23 @@ namespace TestNs { - /// - /// Partial class for the TestVM which contains ReactiveUI Reactive property initialization. - /// - public partial class TestVM - { - [global::System.CodeDom.Compiler.GeneratedCode("ReactiveUI.SourceGenerators.ReactiveGenerator", "1.1.0.0")] - /// - [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - [System.Text.Json.Serialization.JsonInclude] - [System.Runtime.Serialization.DataMember] - public int Test3 - { - get => _test3; - [global::System.Diagnostics.CodeAnalysis.MemberNotNull("_test3")] - set => this.RaiseAndSetIfChanged(ref _test3, value); - } - } + /// + /// Partial class for the TestVM which contains ReactiveUI Reactive property initialization. + /// + public partial class TestVM + { + [global::System.CodeDom.Compiler.GeneratedCode("ReactiveUI.SourceGenerators.ReactiveGenerator", "1.1.0.0")] + /// + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.Runtime.Serialization.DataMemberAttribute()] + [global::System.Text.Json.Serialization.JsonIncludeAttribute()] + public int Test3 + { + get => _test3; + [global::System.Diagnostics.CodeAnalysis.MemberNotNull("_test3")] + set => this.RaiseAndSetIfChanged(ref _test3, value); + } + } } #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactiveProperties#TestVM.Properties.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactiveProperties#TestVM.Properties.g.verified.cs index fe34cd1..9609bc6 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactiveProperties#TestVM.Properties.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactiveProperties#TestVM.Properties.g.verified.cs @@ -7,21 +7,21 @@ namespace TestNs { - /// - /// Partial class for the TestVM which contains ReactiveUI Reactive property initialization. - /// - public partial class TestVM - { - [global::System.CodeDom.Compiler.GeneratedCode("ReactiveUI.SourceGenerators.ReactiveGenerator", "1.1.0.0")] - /// - [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public int Test1 - { - get => _test1; - [global::System.Diagnostics.CodeAnalysis.MemberNotNull("_test1")] - set => this.RaiseAndSetIfChanged(ref _test1, value); - } - } + /// + /// Partial class for the TestVM which contains ReactiveUI Reactive property initialization. + /// + public partial class TestVM + { + [global::System.CodeDom.Compiler.GeneratedCode("ReactiveUI.SourceGenerators.ReactiveGenerator", "1.1.0.0")] + /// + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public int Test1 + { + get => _test1; + [global::System.Diagnostics.CodeAnalysis.MemberNotNull("_test1")] + set => this.RaiseAndSetIfChanged(ref _test1, value); + } + } } #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesWithAccess#TestVM.Properties.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesWithAccess#TestVM.Properties.g.verified.cs index 155c12f..d58d50c 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesWithAccess#TestVM.Properties.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesWithAccess#TestVM.Properties.g.verified.cs @@ -7,21 +7,21 @@ namespace TestNs { - /// - /// Partial class for the TestVM which contains ReactiveUI Reactive property initialization. - /// - public partial class TestVM - { - [global::System.CodeDom.Compiler.GeneratedCode("ReactiveUI.SourceGenerators.ReactiveGenerator", "1.1.0.0")] - /// - [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public int Test2 - { - get => _test2; - [global::System.Diagnostics.CodeAnalysis.MemberNotNull("_test2")] - set => this.RaiseAndSetIfChanged(ref _test2, value); - } - } + /// + /// Partial class for the TestVM which contains ReactiveUI Reactive property initialization. + /// + public partial class TestVM + { + [global::System.CodeDom.Compiler.GeneratedCode("ReactiveUI.SourceGenerators.ReactiveGenerator", "1.1.0.0")] + /// + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public int Test2 + { + get => _test2; + [global::System.Diagnostics.CodeAnalysis.MemberNotNull("_test2")] + set => this.RaiseAndSetIfChanged(ref _test2, value); + } + } } #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/ReactiveUI.SourceGenerators.Tests.csproj b/src/ReactiveUI.SourceGenerator.Tests/ReactiveUI.SourceGenerators.Tests.csproj index 2196043..ce8b968 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/ReactiveUI.SourceGenerators.Tests.csproj +++ b/src/ReactiveUI.SourceGenerator.Tests/ReactiveUI.SourceGenerators.Tests.csproj @@ -25,10 +25,6 @@ - - - - @@ -40,4 +36,8 @@ + + + + diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveGeneratorTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveGeneratorTests.cs index 4961598..4acf69f 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveGeneratorTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveGeneratorTests.cs @@ -94,7 +94,7 @@ namespace TestNs; public partial class TestVM : ReactiveObject { - [JsonInclude] + [property: JsonInclude] [DataMember] [Reactive] private int _test3 = 10; diff --git a/src/ReactiveUI.SourceGenerators.CodeFixers/AnalyzerReleases.Shipped.md b/src/ReactiveUI.SourceGenerators.CodeFixers/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000..137ffd1 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.CodeFixers/AnalyzerReleases.Shipped.md @@ -0,0 +1,66 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +## Release 1.0 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +RXUISG0016 | ReactiveUI.SourceGenerators.PropertyToReactiveFieldCodeFixProvider | Info | See https://www.reactiveui.net/docs/handbook/view-models/boilerplate-code.html + + +## Rules +Shipped in ReactiveUI.SourceGenerators + +- RXUISG0001 - Unsupported C# Language Version +This rule checks if the project is using an unsupported C# language version. The supported versions are C# 12.0 and above. If the project is using an unsupported version, the rule will raise an error. + +- RXUISG0002 - ReactiveCommandGenerator +This rule checks if the `ReactiveCommand` has a Invalid ReactiveCommand method signature. + +- RXUISG0003 - ReactiveCommandGenerator +This rule checks if the `ReactiveCommand` has a Invalid ReactiveCommand.CanExecute member name. + +- RXUISG0004 - ReactiveCommandGenerator +This rule checks if the `ReactiveCommand` has Multiple ReactiveCommand.CanExecute member name matches. + +- RXUISG0005 - ReactiveCommandGenerator +This rule checks if the `ReactiveCommand` has No valid ReactiveCommand.CanExecute member match. + +- RXUISG0006 - ReactiveCommandGenerator +This rule checks if the `ReactiveCommand` has Invalid field or property targeted attribute type. + +- RXUISG0007 - ReactiveCommandGenerator +This rule checks if the `ReactiveCommand` has Invalid field or property targeted attribute expression. + +- RXUISG0008 - AsyncVoidReturningReactiveCommandMethodAnalyzer +This rule checks if the `ReactiveCommand` has Async void returning method annotated with ReactiveCommand. + +- RXUISG0009 - ReactiveGenerator +This rule checks if the `Reactive` has Name collision for generated property. + +- RXUISG0010 - ReactiveGenerator +This rule checks if the `Reactive` has Invalid property targeted attribute type. + +- RXUISG0011 - ReactiveGenerator +This rule checks if the `Reactive` has Invalid property targeted attribute expression. + +- RXUISG0012 - ObservableAsPropertyGenerator +This rule checks if the `ObservableAsProperty` has Invalid property targeted attribute type. + +- RXUISG0013 - ObservableAsPropertyGenerator +This rule checks if the `ObservableAsProperty` has Invalid property targeted attribute expression. + +- RXUISG0014 - ObservableAsPropertyGenerator +This rule checks if the `ObservableAsProperty` has Invalid generated property declaration. + +- RXUISG0015 - ReactiveGenerator +This rule checks if the `Reactive` attribute is being used correctly. If the `Reactive` has Invalid generated property declaration. + +- RXUISG0016 - PropertyToReactiveFieldCodeFixProvider +This rule checks if there are any Properties to change to Reactive Field, change to [Reactive] private type _fieldName;. + +- RXUISG0017 - ObservableAsPropertyFromObservableGenerator +This rule checks if the `ObservableAsProperty` has Invalid generated property declaration. + diff --git a/src/ReactiveUI.SourceGenerators.CodeFixers/AnalyzerReleases.Unshipped.md b/src/ReactiveUI.SourceGenerators.CodeFixers/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..11aac41 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.CodeFixers/AnalyzerReleases.Unshipped.md @@ -0,0 +1,2 @@ +; Unshipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md diff --git a/src/ReactiveUI.SourceGenerators.CodeFixers/Diagnostics/CodeFixers/PropertyToReactiveFieldAnalyzer.cs b/src/ReactiveUI.SourceGenerators.CodeFixers/Diagnostics/CodeFixers/PropertyToReactiveFieldAnalyzer.cs new file mode 100644 index 0000000..0600275 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.CodeFixers/Diagnostics/CodeFixers/PropertyToReactiveFieldAnalyzer.cs @@ -0,0 +1,67 @@ +// 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.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using static ReactiveUI.SourceGenerators.CodeFixers.Diagnostics.DiagnosticDescriptors; + +namespace ReactiveUI.SourceGenerators.CodeFixers; + +/// +/// PropertyToFieldAnalyzer. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class PropertyToReactiveFieldAnalyzer : DiagnosticAnalyzer +{ + /// + /// Gets the supported diagnostics. + /// + /// + /// The supported diagnostics. + /// + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create(PropertyToReactiveFieldRule); + + /// + /// Initializes the specified context. + /// + /// The context. + public override void Initialize(AnalysisContext context) + { + if (context is null) + { + throw new System.ArgumentNullException(nameof(context)); + } + + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.PropertyDeclaration); + } + + private void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + if (context.Node is not PropertyDeclarationSyntax propertyDeclaration) + { + 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 namesToIgnore = new List { "ReactiveCommand", "ReactiveProperty", "ViewModelActivator" }; + var isNotIgnored = !namesToIgnore.Any(n => propertyDeclaration.Type.ToString().Contains(n)); + + if (isAutoProperty && hasCorrectModifiers && doesNotHavePrivateSetOrInternalSet && isNotIgnored) + { + var diagnostic = Diagnostic.Create(PropertyToReactiveFieldRule, propertyDeclaration.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + } +} diff --git a/src/ReactiveUI.SourceGenerators.CodeFixers/Diagnostics/CodeFixers/PropertyToReactiveFieldCodeFixProvider.cs b/src/ReactiveUI.SourceGenerators.CodeFixers/Diagnostics/CodeFixers/PropertyToReactiveFieldCodeFixProvider.cs new file mode 100644 index 0000000..6467571 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.CodeFixers/Diagnostics/CodeFixers/PropertyToReactiveFieldCodeFixProvider.cs @@ -0,0 +1,87 @@ +// 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.Composition; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static ReactiveUI.SourceGenerators.CodeFixers.Diagnostics.DiagnosticDescriptors; + +namespace ReactiveUI.SourceGenerators.CodeFixers; + +/// +/// PropertyToFieldCodeFixProvider. +/// +/// +[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(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; + + /// + /// 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(); + + 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"))))); + + 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))); + + // 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); + } +} diff --git a/src/ReactiveUI.SourceGenerators.CodeFixers/Diagnostics/DiagnosticDescriptors.cs b/src/ReactiveUI.SourceGenerators.CodeFixers/Diagnostics/DiagnosticDescriptors.cs new file mode 100644 index 0000000..9df1a22 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.CodeFixers/Diagnostics/DiagnosticDescriptors.cs @@ -0,0 +1,29 @@ +// 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; + +#pragma warning disable IDE0090 // Use 'new DiagnosticDescriptor(...)' + +namespace ReactiveUI.SourceGenerators.CodeFixers.Diagnostics; + +/// +/// A container for all instances for errors reported by analyzers in this project. +/// +internal static class DiagnosticDescriptors +{ + /// + /// The property to field rule. + /// + public static readonly DiagnosticDescriptor PropertyToReactiveFieldRule = new( + id: "RXUISG0016", + title: "Property To Reactive Field, change to [Reactive] private type _fieldName;", + messageFormat: "Replace the property with a INPC Reactive Property for ReactiveUI", + category: typeof(PropertyToReactiveFieldAnalyzer).FullName, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: "Used to create a Read Write INPC Reactive Property for ReactiveUI, annotated with [Reactive].", + helpLinkUri: "https://www.reactiveui.net/docs/handbook/view-models/boilerplate-code.html"); +} diff --git a/src/ReactiveUI.SourceGenerators.CodeFixers/ReactiveUI.CodeFixes.csproj b/src/ReactiveUI.SourceGenerators.CodeFixers/ReactiveUI.CodeFixes.csproj new file mode 100644 index 0000000..0255b03 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.CodeFixers/ReactiveUI.CodeFixes.csproj @@ -0,0 +1,23 @@ + + + + netstandard2.0 + enable + true + true + latest + true + true + false + 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 + + false + + ..\ReactiveUI.SourceGenerators\bin\ + + + + + + + diff --git a/src/ReactiveUI.SourceGenerators.Execute.Maui/ReactiveUI.SourceGenerators.Execute.Maui.csproj b/src/ReactiveUI.SourceGenerators.Execute.Maui/ReactiveUI.SourceGenerators.Execute.Maui.csproj index 8b109b6..986863f 100644 --- a/src/ReactiveUI.SourceGenerators.Execute.Maui/ReactiveUI.SourceGenerators.Execute.Maui.csproj +++ b/src/ReactiveUI.SourceGenerators.Execute.Maui/ReactiveUI.SourceGenerators.Execute.Maui.csproj @@ -1,30 +1,31 @@  - - net8.0-android;net8.0-ios;net8.0-maccatalyst - $(TargetFrameworks);net8.0-windows10.0.19041.0 - true - enable - enable + + net8.0-android;net8.0-ios;net8.0-maccatalyst + $(TargetFrameworks);net8.0-windows10.0.19041.0 + true + enable + enable false latest - 11.0 - 13.1 - 21.0 - 10.0.17763.0 - 10.0.17763.0 - 6.5 - + 11.0 + 13.1 + 21.0 + 10.0.17763.0 + 10.0.17763.0 + 6.5 + - + - - - - - - - + + + + + + + + diff --git a/src/ReactiveUI.SourceGenerators.Execute/ReactiveUI.SourceGenerators.Execute.csproj b/src/ReactiveUI.SourceGenerators.Execute/ReactiveUI.SourceGenerators.Execute.csproj index 5e66316..6c34648 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/ReactiveUI.SourceGenerators.Execute.csproj +++ b/src/ReactiveUI.SourceGenerators.Execute/ReactiveUI.SourceGenerators.Execute.csproj @@ -13,10 +13,11 @@ - + - + + diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs index 85b3621..72e7230 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs @@ -17,9 +17,9 @@ namespace SGReactiveUI.SourceGenerators.Test; /// /// TestClass. /// -/// -/// -/// +/// +/// +/// [DataContract] public partial class TestViewModel : ReactiveObject, IActivatableViewModel, IDisposable { @@ -35,11 +35,12 @@ public partial class TestViewModel : ReactiveObject, IActivatableViewModel, IDis [ObservableAsProperty(ReadOnly = false)] private double? _test11Property = 11.1d; + [property: Test(AParameter = "Test Input")] [Reactive] private double? _test12Property = 12.1d; - [JsonInclude] [Reactive(SetModifier = AccessModifier.Protected)] + [property: JsonInclude] [DataMember] private int _test1Property; private bool _disposedValue; diff --git a/src/ReactiveUI.SourceGenerators.sln b/src/ReactiveUI.SourceGenerators.sln index 7da20f8..eb7af8c 100644 --- a/src/ReactiveUI.SourceGenerators.sln +++ b/src/ReactiveUI.SourceGenerators.sln @@ -22,6 +22,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.SourceGenerators EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.SourceGenerators.Tests", "ReactiveUI.SourceGenerator.Tests\ReactiveUI.SourceGenerators.Tests.csproj", "{60C7C031-8765-46D7-8E0D-88CD4949F25B}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestLibs", "TestLibs", "{B86ED9C1-AFFB-4854-AD80-F4B4050CAD0A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.CodeFixes", "ReactiveUI.SourceGenerators.CodeFixers\ReactiveUI.CodeFixes.csproj", "{C89EE66E-E1DC-4A31-9322-20D95CB0D74D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -44,10 +48,18 @@ Global {60C7C031-8765-46D7-8E0D-88CD4949F25B}.Debug|Any CPU.Build.0 = Debug|Any CPU {60C7C031-8765-46D7-8E0D-88CD4949F25B}.Release|Any CPU.ActiveCfg = Release|Any CPU {60C7C031-8765-46D7-8E0D-88CD4949F25B}.Release|Any CPU.Build.0 = Release|Any CPU + {C89EE66E-E1DC-4A31-9322-20D95CB0D74D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C89EE66E-E1DC-4A31-9322-20D95CB0D74D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C89EE66E-E1DC-4A31-9322-20D95CB0D74D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C89EE66E-E1DC-4A31-9322-20D95CB0D74D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {76D5AC8C-4935-3E4B-BD12-71FAEC2B9A9D} = {B86ED9C1-AFFB-4854-AD80-F4B4050CAD0A} + {849CACF4-B85F-47B5-84B3-7C94DE864E7E} = {B86ED9C1-AFFB-4854-AD80-F4B4050CAD0A} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {173F891B-86A2-4226-B563-A7318CE0E2EC} EndGlobalSection diff --git a/src/ReactiveUI.SourceGenerators/AnalyzerReleases.Shipped.md b/src/ReactiveUI.SourceGenerators/AnalyzerReleases.Shipped.md index 8e2ed2b..f4ab2a1 100644 --- a/src/ReactiveUI.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/ReactiveUI.SourceGenerators/AnalyzerReleases.Shipped.md @@ -22,7 +22,6 @@ RXUISG0012 | ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator | Error | RXUISG0013 | ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator | Error | See https://www.reactiveui.net/docs/handbook/view-models/boilerplate-code.html RXUISG0014 | ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator | Error | See https://www.reactiveui.net/docs/handbook/view-models/boilerplate-code.html RXUISG0015 | ReactiveUI.SourceGenerators.ReactiveGenerator | Error | See https://www.reactiveui.net/docs/handbook/view-models/boilerplate-code.html -RXUISG0016 | ReactiveUI.SourceGenerators.PropertyToReactiveFieldCodeFixProvider | Info | See https://www.reactiveui.net/docs/handbook/view-models/boilerplate-code.html RXUISG0017 | ReactiveUI.SourceGenerators.ObservableAsPropertyFromObservableGenerator | Error | See https://www.reactiveui.net/docs/handbook/view-models/boilerplate-code.html diff --git a/src/ReactiveUI.SourceGenerators/Core/Extensions/AttributeDataExtensions.cs b/src/ReactiveUI.SourceGenerators/Core/Extensions/AttributeDataExtensions.cs index fdf577c..a7e1331 100644 --- a/src/ReactiveUI.SourceGenerators/Core/Extensions/AttributeDataExtensions.cs +++ b/src/ReactiveUI.SourceGenerators/Core/Extensions/AttributeDataExtensions.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using ReactiveUI.SourceGenerators.Helpers; using ReactiveUI.SourceGenerators.Models; @@ -254,6 +255,147 @@ static void GatherForwardedAttributes( classAttributesInfo = classAttributesInfoBuilder.ToImmutable(); } + /// + /// 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. + public static void GatherForwardedAttributesFromMethod( + this IMethodSymbol methodSymbol, + SemanticModel semanticModel, + MethodDeclarationSyntax methodDeclaration, + CancellationToken token, + out ImmutableArray propertyAttributeInfos) + { + using var propertyAttributesInfo = ImmutableArrayBuilder.Rent(); + + static void GatherForwardedAttributesFromMethod( + IMethodSymbol methodSymbol, + SemanticModel semanticModel, + MethodDeclarationSyntax methodDeclaration, + CancellationToken token, + ImmutableArrayBuilder propertyAttributesInfos) + { + // 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)) + { + propertyAttributesInfos.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); + } + + propertyAttributeInfos = propertyAttributesInfo.ToImmutable(); + } + + public static void GatherForwardedAttributesFromProperty( + this IPropertySymbol propertySymbol, + SemanticModel semanticModel, + PropertyDeclarationSyntax propertyDeclaration, + CancellationToken token, + out ImmutableArray propertyAttributesInfos) + { + using var propertyAttributesInfo = ImmutableArrayBuilder.Rent(); + + static void GatherForwardedAttributesFromProperty( + IPropertySymbol propertySymbol, + SemanticModel semanticModel, + PropertyDeclarationSyntax propertyDeclaration, + CancellationToken token, + ImmutableArrayBuilder propertyAttributesInfos) + { + // Get the single syntax reference for the input method symbol (there should be only one) + if (propertySymbol.DeclaringSyntaxReferences is not [SyntaxReference syntaxReference]) + { + return; + } + + // Gather explicit forwarded attributes info + foreach (var attributeList in propertyDeclaration.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)) + { + propertyAttributesInfos.Add(attributeInfo); + } + } + } + } + + // If the method is not a partial definition/implementation, just gather attributes from the method with no modifications + GatherForwardedAttributesFromProperty(propertySymbol, semanticModel, propertyDeclaration, token, propertyAttributesInfo); + + propertyAttributesInfos = propertyAttributesInfo.ToImmutable(); + } + /// /// Gets the type of the generic. /// diff --git a/src/ReactiveUI.SourceGenerators/Core/Extensions/CompilationExtensions.cs b/src/ReactiveUI.SourceGenerators/Core/Extensions/CompilationExtensions.cs index 53a4fb2..b93d8b3 100644 --- a/src/ReactiveUI.SourceGenerators/Core/Extensions/CompilationExtensions.cs +++ b/src/ReactiveUI.SourceGenerators/Core/Extensions/CompilationExtensions.cs @@ -26,6 +26,15 @@ internal static class CompilationExtensions public static bool HasLanguageVersionAtLeastEqualTo(this Compilation compilation, LanguageVersion languageVersion) => ((CSharpCompilation)compilation).LanguageVersion >= languageVersion; + /// + /// Checks whether a given compilation (assumed to be for C#) is using at least a given language version. + /// + /// The to consider for analysis. + /// The minimum language version to check. + /// Whether is using at least the specified language version. + public static bool HasLanguageVersionAtLeastEqualTo(this Compilation compilation, int languageVersion) => + ((int)((CSharpCompilation)compilation).LanguageVersion) >= languageVersion; + /// /// /// Checks whether or not a type with a specified metadata name is accessible from a given instance. diff --git a/src/ReactiveUI.SourceGenerators/Core/Extensions/DiagnosticsExtensions.cs b/src/ReactiveUI.SourceGenerators/Core/Extensions/DiagnosticsExtensions.cs index f5bb3e0..8603428 100644 --- a/src/ReactiveUI.SourceGenerators/Core/Extensions/DiagnosticsExtensions.cs +++ b/src/ReactiveUI.SourceGenerators/Core/Extensions/DiagnosticsExtensions.cs @@ -10,7 +10,7 @@ namespace ReactiveUI.SourceGenerators.Extensions; /// -/// Extension methods for , specifically for reporting diagnostics. +/// Extension methods for , specifically for reporting diagnostics. /// internal static class DiagnosticsExtensions { diff --git a/src/ReactiveUI.SourceGenerators/Core/Extensions/FieldSyntaxExtensions.cs b/src/ReactiveUI.SourceGenerators/Core/Extensions/FieldSyntaxExtensions.cs index f27e0d7..cead10e 100644 --- a/src/ReactiveUI.SourceGenerators/Core/Extensions/FieldSyntaxExtensions.cs +++ b/src/ReactiveUI.SourceGenerators/Core/Extensions/FieldSyntaxExtensions.cs @@ -30,4 +30,46 @@ internal static string GetGeneratedPropertyName(this IFieldSymbol fieldSymbol) return $"{char.ToUpper(propertyName[0], CultureInfo.InvariantCulture)}{propertyName.Substring(1)}"; } + + /// + /// Gets the nullability info on the generated property. + /// + /// The input instance to process. + /// The instance for the current run. + /// Whether the property type supports nullability. + /// Whether MemberNotNullAttribute should be used on the setter. + internal static void GetNullabilityInfo( + this IFieldSymbol fieldSymbol, + SemanticModel semanticModel, + out bool isReferenceTypeOrUnconstraindTypeParameter, + out bool includeMemberNotNullOnSetAccessor) + { + // We're using IsValueType here and not IsReferenceType to also cover unconstrained type parameter cases. + // This will cover both reference types as well T when the constraints are not struct or unmanaged. + // If this is true, it means the field storage can potentially be in a null state (even if not annotated). + isReferenceTypeOrUnconstraindTypeParameter = !fieldSymbol.Type.IsValueType; + + // This is used to avoid nullability warnings when setting the property from a constructor, in case the field + // was marked as not nullable. Nullability annotations are assumed to always be enabled to make the logic simpler. + // Consider this example: + // + // partial class MyViewModel : ReactiveObject + // { + // public MyViewModel() + // { + // Name = "Bob"; + // } + // + // [Reactive] + // private string _name; + // } + // + // The [MemberNotNull] attribute is needed on the setter for the generated Name property so that when Name + // is set, the compiler can determine that the name backing field is also being set (to a non null value). + // Of course, this can only be the case if the field type is also of a type that could be in a null state. + includeMemberNotNullOnSetAccessor = + isReferenceTypeOrUnconstraindTypeParameter && + fieldSymbol.Type.NullableAnnotation != NullableAnnotation.Annotated && + semanticModel.Compilation.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.MemberNotNullAttribute"); + } } diff --git a/src/ReactiveUI.SourceGenerators/Core/Polyfills/SyntaxValueProviderExtensions.cs b/src/ReactiveUI.SourceGenerators/Core/Polyfills/SyntaxValueProviderExtensions.cs index f260f37..172d6c5 100644 --- a/src/ReactiveUI.SourceGenerators/Core/Polyfills/SyntaxValueProviderExtensions.cs +++ b/src/ReactiveUI.SourceGenerators/Core/Polyfills/SyntaxValueProviderExtensions.cs @@ -65,7 +65,7 @@ public static IncrementalValuesProvider ForAttributeWithMetadataNameInternal< return null; } - if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp11)) + if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(1200)) { return null; } @@ -109,7 +109,7 @@ public static IncrementalValuesProvider ForAllAttributes( return null; } - if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp11)) + if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(1200)) { return null; } diff --git a/src/ReactiveUI.SourceGenerators/Diagnostics/CodeAnalyzers/PropertyToReactiveFieldAnalyzer.cs b/src/ReactiveUI.SourceGenerators/Diagnostics/CodeAnalyzers/PropertyToReactiveFieldAnalyzer.cs deleted file mode 100644 index c4fabcf..0000000 --- a/src/ReactiveUI.SourceGenerators/Diagnostics/CodeAnalyzers/PropertyToReactiveFieldAnalyzer.cs +++ /dev/null @@ -1,67 +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.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; -using ReactiveUI.SourceGenerators.Diagnostics; - -namespace ReactiveUI.SourceGenerators.CodeAnalyzers -{ - /// - /// PropertyToFieldAnalyzer. - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public class PropertyToReactiveFieldAnalyzer : DiagnosticAnalyzer - { - /// - /// Gets the supported diagnostics. - /// - /// - /// The supported diagnostics. - /// - public override ImmutableArray SupportedDiagnostics => - ImmutableArray.Create(DiagnosticDescriptors.PropertyToReactiveFieldRule); - - /// - /// Initializes the specified context. - /// - /// The context. - public override void Initialize(AnalysisContext context) - { - if (context is null) - { - throw new System.ArgumentNullException(nameof(context)); - } - - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.EnableConcurrentExecution(); - context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.PropertyDeclaration); - } - - private void AnalyzeNode(SyntaxNodeAnalysisContext context) - { - if (context.Node is not PropertyDeclarationSyntax propertyDeclaration) - { - 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"); - - 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 deleted file mode 100644 index 313e9cf..0000000 --- a/src/ReactiveUI.SourceGenerators/Diagnostics/CodeAnalyzers/PropertyToReactiveFieldCodeFixProvider.cs +++ /dev/null @@ -1,88 +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.Composition; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CodeActions; -using Microsoft.CodeAnalysis.CodeFixes; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using ReactiveUI.SourceGenerators.Diagnostics; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; - -namespace ReactiveUI.SourceGenerators.CodeAnalyzers -{ - /// - /// PropertyToFieldCodeFixProvider. - /// - /// - [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); - - /// - /// 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; - - // 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 attributeSyntaxes = - propertyDeclaration!.AttributeLists - .Select(static a => AttributeList(a.Attributes)).ToList(); - attributeSyntaxes.Add(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("ReactiveUI.SourceGenerators.Reactive"))))); - - 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))); - - // 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); - } - } -} diff --git a/src/ReactiveUI.SourceGenerators/Diagnostics/CodeAnalyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs b/src/ReactiveUI.SourceGenerators/Diagnostics/CodeAnalyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs index 015d627..f1eb6be 100644 --- a/src/ReactiveUI.SourceGenerators/Diagnostics/CodeAnalyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs +++ b/src/ReactiveUI.SourceGenerators/Diagnostics/CodeAnalyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs @@ -8,7 +8,6 @@ using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; using ReactiveUI.SourceGenerators.Extensions; using static ReactiveUI.SourceGenerators.Diagnostics.DiagnosticDescriptors; @@ -51,7 +50,7 @@ public override void Initialize(AnalysisContext context) context.RegisterCompilationStartAction(static context => { // Check that the language version is not high enough, otherwise no diagnostic should ever be produced - if (context.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp9)) + if (context.Compilation.HasLanguageVersionAtLeastEqualTo(1200)) { return; } diff --git a/src/ReactiveUI.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/ReactiveUI.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 805cd0d..fd22411 100644 --- a/src/ReactiveUI.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/ReactiveUI.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -4,7 +4,6 @@ // See the LICENSE file in the project root for full license information. using Microsoft.CodeAnalysis; -using ReactiveUI.SourceGenerators.CodeAnalyzers; #pragma warning disable IDE0090 // Use 'new DiagnosticDescriptor(...)' @@ -20,12 +19,12 @@ internal static class DiagnosticDescriptors /// public static readonly DiagnosticDescriptor UnsupportedCSharpLanguageVersionError = new DiagnosticDescriptor( id: "RXUISG0001", - title: "Unsupported C# language version", - messageFormat: "The source generator features from ReactiveUI require consuming projects to set the C# language version to at least C# 9.0", + title: "Unsupported C# language version (< 12.0)", + messageFormat: "The source generator features from ReactiveUI require consuming projects to set the C# language version to at least C# 12.0", category: typeof(UnsupportedCSharpLanguageVersionAnalyzer).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "The source generator features from ReactiveUI require consuming projects to set the C# language version to at least C# 9.0. Make sure to add 9.0 (or above) to your .csproj file.", + description: "The source generator features from ReactiveUI require consuming projects to set the C# language version to at least C# 12.0. Make sure to add 12.0 (or above) to your .csproj file.", helpLinkUri: "https://www.reactiveui.net/docs/handbook/view-models/boilerplate-code.html"); /// @@ -38,7 +37,7 @@ internal static class DiagnosticDescriptors id: "RXUISG0002", title: "Invalid ReactiveCommand method signature", messageFormat: "The method {0}.{1} cannot be used to generate a command property, as its signature isn't compatible with any of the existing reactive command types", - category: typeof(ReactiveCommandGenerator).FullName, + category: "ReactiveUI.SourceGenerators.ReactiveCommandGenerator", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "Cannot apply [ReactiveCommand] to methods with a signature that doesn't match any of the existing reactive command types.", @@ -54,7 +53,7 @@ internal static class DiagnosticDescriptors id: "RXUISG0003", title: "Invalid ReactiveCommand.CanExecute member name", messageFormat: "The CanExecute name must refer to a valid member, but \"{0}\" has no matches in type {1}", - category: typeof(ReactiveCommandGenerator).FullName, + category: "ReactiveUI.SourceGenerators.ReactiveCommandGenerator", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "The CanExecute name in [ReactiveCommand] must refer to a valid member in its parent type.", @@ -70,7 +69,7 @@ internal static class DiagnosticDescriptors id: "RXUISG0004", title: "Multiple ReactiveCommand.CanExecute member name matches", messageFormat: "The CanExecute name must refer to a single member, but \"{0}\" has multiple matches in type {1}", - category: typeof(ReactiveCommandGenerator).FullName, + category: "ReactiveUI.SourceGenerators.ReactiveCommandGenerator", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "Cannot set the CanExecute name in [ReactiveCommand] to one that has multiple matches in its parent type (it must refer to a single compatible member).", @@ -86,7 +85,7 @@ internal static class DiagnosticDescriptors id: "RXUISG0005", title: "No valid ReactiveCommand.CanExecute member match", messageFormat: "The CanExecute name must refer to a compatible member, but no valid members were found for \"{0}\" in type {1}", - category: typeof(ReactiveCommandGenerator).FullName, + category: "ReactiveUI.SourceGenerators.ReactiveCommandGenerator", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "The CanExecute name in [ReactiveCommand] must refer to a compatible member (either a property or a method) to be used in a generated command.", @@ -102,7 +101,7 @@ internal static class DiagnosticDescriptors id: "RXUISG0006", title: "Invalid field or property targeted attribute type", messageFormat: "The method {0} annotated with [ReactiveCommand] is using attribute \"{1}\" which was not recognized as a valid type (are you missing a using directive?)", - category: typeof(ReactiveCommandGenerator).FullName, + category: "ReactiveUI.SourceGenerators.ReactiveCommandGenerator", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "All attributes targeting the generated field or property for a method annotated with [ReactiveCommand] must correctly be resolved to valid types.", @@ -118,7 +117,7 @@ internal static class DiagnosticDescriptors id: "RXUISG0007", title: "Invalid field or property targeted attribute expression", messageFormat: "The method {0} annotated with [ReactiveCommand] is using attribute \"{1}\" with an invalid expression (are you passing any incorrect parameters to the attribute constructor?)", - category: typeof(ReactiveCommandGenerator).FullName, + category: "ReactiveUI.SourceGenerators.ReactiveCommandGenerator", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "All attributes targeting the generated field or property for a method annotated with [ReactiveCommand] must be using valid expressions.", @@ -150,7 +149,7 @@ internal static class DiagnosticDescriptors id: "RXUISG0009", title: "Name collision for generated property", messageFormat: "The field {0}.{1} cannot be used to generate an reactive property, as its name would collide with the field name (instance fields should use the \"lowerCamel\", \"_lowerCamel\" or \"m_lowerCamel\" pattern)", - category: typeof(ReactiveGenerator).FullName, + category: "ReactiveUI.SourceGenerators.ReactiveGenerator", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "The name of fields annotated with [Reactive] should use \"lowerCamel\", \"_lowerCamel\" or \"m_lowerCamel\" pattern to avoid collisions with the generated properties.", @@ -166,7 +165,7 @@ internal static class DiagnosticDescriptors id: "RXUISG0010", title: "Invalid property targeted attribute type", messageFormat: "The field {0} annotated with [Reactive] is using attribute \"{1}\" which was not recognized as a valid type (are you missing a using directive?)", - category: typeof(ReactiveGenerator).FullName, + category: "ReactiveUI.SourceGenerators.ReactiveGenerator", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "All attributes targeting the generated property for a field annotated with [Reactive] must correctly be resolved to valid types.", @@ -182,7 +181,7 @@ internal static class DiagnosticDescriptors id: "RXUISG0011", title: "Invalid property targeted attribute expression", messageFormat: "The field {0} annotated with [Reactive] is using attribute \"{1}\" with an invalid expression (are you passing any incorrect parameters to the attribute constructor?)", - category: typeof(ReactiveGenerator).FullName, + category: "ReactiveUI.SourceGenerators.ReactiveGenerator", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "All attributes targeting the generated property for a field annotated with [Reactive] must be using valid expressions.", @@ -198,7 +197,7 @@ internal static class DiagnosticDescriptors id: "RXUISG0012", title: "Invalid property targeted attribute type", messageFormat: "The field {0} annotated with [ObservableAsProperty] is using attribute \"{1}\" which was not recognized as a valid type (are you missing a using directive?)", - category: typeof(ObservableAsPropertyGenerator).FullName, + category: "ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "All attributes targeting the generated property for a field annotated with [ObservableAsProperty] must correctly be resolved to valid types.", @@ -214,7 +213,7 @@ internal static class DiagnosticDescriptors id: "RXUISG0013", title: "Invalid property targeted attribute expression", messageFormat: "The field {0} annotated with [ObservableAsProperty] is using attribute \"{1}\" with an invalid expression (are you passing any incorrect parameters to the attribute constructor?)", - category: typeof(ObservableAsPropertyGenerator).FullName, + category: "ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "All attributes targeting the generated property for a field annotated with [ObservableAsProperty] must be using valid expressions.", @@ -230,7 +229,7 @@ internal static class DiagnosticDescriptors id: "RXUISG0014", title: "Invalid generated property declaration", messageFormat: "The field {0}.{1} cannot be used to generate an observable As property, as its name or type would cause conflicts with other generated members", - category: typeof(ObservableAsPropertyGenerator).FullName, + category: "ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "The fields annotated with [ObservableAsProperty] cannot result in a property name or have a type that would cause conflicts with other generated members.", @@ -246,25 +245,12 @@ internal static class DiagnosticDescriptors id: "RXUISG0015", title: "Invalid generated property declaration", messageFormat: "The field {0}.{1} cannot be used to generate an reactive property, as its name or type would cause conflicts with other generated members", - category: typeof(ReactiveGenerator).FullName, + category: "ReactiveUI.SourceGenerators.ReactiveGenerator", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "The fields annotated with [Reactive] cannot result in a property name or have a type that would cause conflicts with other generated members.", helpLinkUri: "https://www.reactiveui.net/docs/handbook/view-models/boilerplate-code.html"); - /// - /// The property to field rule. - /// - public static readonly DiagnosticDescriptor PropertyToReactiveFieldRule = new( - id: "RXUISG0016", - title: "Property To Reactive Field, change to [Reactive] private type _fieldName;", - messageFormat: "Replace the property with a INPC Reactive Property for ReactiveUI", - category: typeof(PropertyToReactiveFieldAnalyzer).FullName, - defaultSeverity: DiagnosticSeverity.Info, - isEnabledByDefault: true, - description: "Used to create a Read Write INPC Reactive Property for ReactiveUI, annotated with [Reactive].", - helpLinkUri: "https://www.reactiveui.net/docs/handbook/view-models/boilerplate-code.html"); - /// /// The observable as property method has parameters error. /// @@ -272,7 +258,7 @@ internal static class DiagnosticDescriptors id: "RXUISG0017", title: "Invalid generated property declaration", messageFormat: "The method {0} cannot be used to generate an observable As property, as it has parameters", - category: typeof(ObservableAsPropertyGenerator).FullName, + category: "ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "The method annotated with [ObservableAsProperty] cannot currently initialize methods with parameters.", diff --git a/src/ReactiveUI.SourceGenerators/Diagnostics/SuppressionDescriptors.cs b/src/ReactiveUI.SourceGenerators/Diagnostics/SuppressionDescriptors.cs index e61c8b3..1b1e23e 100644 --- a/src/ReactiveUI.SourceGenerators/Diagnostics/SuppressionDescriptors.cs +++ b/src/ReactiveUI.SourceGenerators/Diagnostics/SuppressionDescriptors.cs @@ -28,4 +28,9 @@ internal static class SuppressionDescriptors id: "RXUISPR0004", suppressedDiagnosticId: "RCS1169", justification: "Fields using [Reactive] do not need to be ReadOnly"); + + public static readonly SuppressionDescriptor FieldOrPropertyAttributeListForReactiveProperty = new( + id: "RXUISPR0005", + suppressedDiagnosticId: "CS0657", + justification: "Fields using [Reactive] can use [property:] attribute lists to forward attributes to the generated properties"); } diff --git a/src/ReactiveUI.SourceGenerators/Diagnostics/Suppressions/ReactiveAttributeWithPropertyTargetDiagnosticSuppressor.cs b/src/ReactiveUI.SourceGenerators/Diagnostics/Suppressions/ReactiveAttributeWithPropertyTargetDiagnosticSuppressor.cs new file mode 100644 index 0000000..3727542 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators/Diagnostics/Suppressions/ReactiveAttributeWithPropertyTargetDiagnosticSuppressor.cs @@ -0,0 +1,48 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using ReactiveUI.SourceGenerators.Extensions; +using ReactiveUI.SourceGenerators.Helpers; +using static ReactiveUI.SourceGenerators.Diagnostics.SuppressionDescriptors; + +namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions +{ + /// + /// ReactiveCommand Attribute With Field Or Property Target Diagnostic Suppressor. + /// + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class ReactiveAttributeWithPropertyTargetDiagnosticSuppressor : DiagnosticSuppressor + { + /// + public override ImmutableArray SupportedSuppressions => ImmutableArray.Create(FieldOrPropertyAttributeListForReactiveProperty); + + /// + public override void ReportSuppressions(SuppressionAnalysisContext context) + { + foreach (var diagnostic in context.ReportedDiagnostics) + { + var syntaxNode = diagnostic.Location.SourceTree?.GetRoot(context.CancellationToken).FindNode(diagnostic.Location.SourceSpan); + + // Check that the target is effectively [property:] over a method declaration, which is the case we're looking for + if (syntaxNode is AttributeTargetSpecifierSyntax { Parent.Parent: FieldDeclarationSyntax, Identifier: SyntaxToken(SyntaxKind.PropertyKeyword) }) + { + var semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree); + + // Check if the method is using [Reactive], in which case we should suppress the warning + if (semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ReactiveAttributeType) is not null) + { + context.ReportSuppression(Suppression.Create(FieldOrPropertyAttributeListForReactiveProperty, diagnostic)); + } + } + } + } + } +} diff --git a/src/ReactiveUI.SourceGenerators/IViewFor/IViewForGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators/IViewFor/IViewForGenerator.Execute.cs index 268289a..ab1a640 100644 --- a/src/ReactiveUI.SourceGenerators/IViewFor/IViewForGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators/IViewFor/IViewForGenerator.Execute.cs @@ -112,7 +112,7 @@ public partial class IViewForGenerator private static string GenerateSource(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, IViewForInfo iviewForInfo) { // Prepare any forwarded property attributes - var forwardedAttributesString = string.Join("\n\t\t", excludeFromCodeCoverage.Concat(iviewForInfo.ForwardedAttributes)); + var forwardedAttributesString = string.Join("\n ", excludeFromCodeCoverage.Concat(iviewForInfo.ForwardedAttributes)); switch (iviewForInfo.BaseType) { diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.Execute.cs index 1e1bb04..0dbbfb8 100644 --- a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.Execute.cs @@ -6,11 +6,9 @@ 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; @@ -21,7 +19,7 @@ namespace ReactiveUI.SourceGenerators; /// /// Observable As Property From Observable Generator. /// -/// +/// public sealed partial class ObservableAsPropertyGenerator { internal static partial class Execute @@ -120,165 +118,6 @@ internal static MethodDeclarationSyntax GetPropertyInitiliser(ObservableMethodIn .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; diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.cs index 5a38437..4e6845b 100644 --- a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.cs +++ b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.cs @@ -5,12 +5,10 @@ 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; @@ -63,16 +61,15 @@ private static void RunObservablePropertyAsFromObservable(in IncrementalGenerato return default; } - var isObservable = Execute.IsObservableReturnType(methodSymbol.ReturnType); + var isObservable = methodSymbol.ReturnType.IsObservableReturnType(); token.ThrowIfCancellationRequested(); - Execute.GatherForwardedAttributesFromMethod( - methodSymbol, - context.SemanticModel, - methodSyntax, - token, - out var propertyAttributes); + methodSymbol.GatherForwardedAttributesFromMethod( + context.SemanticModel, + methodSyntax, + token, + out var propertyAttributes); token.ThrowIfCancellationRequested(); @@ -92,16 +89,15 @@ private static void RunObservablePropertyAsFromObservable(in IncrementalGenerato if (context.TargetNode is PropertyDeclarationSyntax propertySyntax) { var propertySymbol = (IPropertySymbol)symbol!; - var isObservable = Execute.IsObservableReturnType(propertySymbol.Type); + var isObservable = propertySymbol.Type.IsObservableReturnType(); token.ThrowIfCancellationRequested(); - Execute.GatherForwardedAttributesFromProperty( - propertySymbol, - context.SemanticModel, - propertySyntax, - token, - out var propertyAttributes); + propertySymbol.GatherForwardedAttributesFromProperty( + context.SemanticModel, + propertySyntax, + token, + out var propertyAttributes); token.ThrowIfCancellationRequested(); diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Generator.Execute.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Generator.Execute.cs index 10e7ac3..88acd08 100644 --- a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Generator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Generator.Execute.cs @@ -75,24 +75,19 @@ internal static ImmutableArray GetPropertySyntax(Proper getterArrowExpression = ArrowExpressionClause(ParseExpression($"{getterFieldIdentifierName} = {getterFieldIdentifierName}Helper?.Value ?? {getterFieldIdentifierName}")); } - ////// Prepare the forwarded attributes, if any - ////var forwardedAttributes = - //// propertyInfo.ForwardedAttributes - //// .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) - //// .ToImmutableArray(); - - var modifiers = new List(); - var helperTypeName = $"ReactiveUI.ObservableAsPropertyHelper<{propertyType}>"; + // Prepare the forwarded attributes, if any + var forwardedAttributes = + propertyInfo.ForwardedAttributes + .Select(static a => AttributeList(SingletonSeparatedList(Attribute(ParseName(a.Substring(1, a.Length - 2)))))) + .ToImmutableArray(); + + var modifiers = new List { Token(SyntaxKind.PrivateKeyword) }; + var helperTypeName = $"ReactiveUI.ObservableAsPropertyHelper<{propertyType}>?"; if (propertyInfo.AccessModifier == "readonly") { - modifiers.Add(Token(SyntaxKind.PrivateKeyword)); + helperTypeName = $"ReactiveUI.ObservableAsPropertyHelper<{propertyType}>"; modifiers.Add(Token(SyntaxKind.ReadOnlyKeyword)); } - else - { - helperTypeName = $"ReactiveUI.ObservableAsPropertyHelper<{propertyType}>?"; - modifiers.Add(Token(SyntaxKind.PrivateKeyword)); - } // Construct the generated property as follows: // @@ -125,7 +120,7 @@ internal static ImmutableArray GetPropertySyntax(Proper 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]) + .AddAttributeLists([.. forwardedAttributes]) .AddModifiers(Token(SyntaxKind.PublicKeyword)) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) @@ -163,7 +158,7 @@ internal static bool GetFieldInfoFromClass( var typeNameWithNullabilityAnnotations = fieldSymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); var fieldName = fieldSymbol.Name; var propertyName = GetGeneratedPropertyName(fieldSymbol); - var initializer = fieldSyntax.Declaration.Variables.FirstOrDefault()?.Initializer; + var initializer = fieldSyntax.Declaration.Variables.FirstOrDefault()?.Initializer?.ToFullString(); // Check for name collisions if (fieldName == propertyName) @@ -280,29 +275,32 @@ internal static bool GetFieldInfoFromClass( token.ThrowIfCancellationRequested(); // Get the nullability info for the property - GetNullabilityInfo( - fieldSymbol, - semanticModel, - out var isReferenceTypeOrUnconstraindTypeParameter, - out var includeMemberNotNullOnSetAccessor); + fieldSymbol.GetNullabilityInfo( + semanticModel, + out var isReferenceTypeOrUnconstraindTypeParameter, + out var includeMemberNotNullOnSetAccessor); token.ThrowIfCancellationRequested(); - PropertyAttributeData[] pd = [new(string.Empty, string.Empty)]; + var attributes = forwardedAttributes.ToImmutable(); + var forwardedPropertyAttributes = attributes.Select(static a => a.ToString()).ToImmutableArray(); + + // Get the containing type info + var targetInfo = TargetInfo.From(fieldSymbol.ContainingType); propertyInfo = new PropertyInfo( - string.Empty, - string.Empty, - string.Empty, - string.Empty, - string.Empty, - string.Empty, + targetInfo.FileHintName, + targetInfo.TargetName, + targetInfo.TargetNamespace, + targetInfo.TargetNamespaceWithNamespace, + targetInfo.TargetVisibility, + targetInfo.TargetType, typeNameWithNullabilityAnnotations, fieldName, propertyName, initializer, isReferenceTypeOrUnconstraindTypeParameter, includeMemberNotNullOnSetAccessor, - new(pd), + forwardedPropertyAttributes, isReadonly == false ? string.Empty : "readonly"); diagnostics = builder.ToImmutable(); @@ -324,48 +322,6 @@ private static bool IsTargetTypeValid(IFieldSymbol fieldSymbol) return isIObservableObject || isObservableObject || hasObservableObjectAttribute; } - /// - /// Gets the nullability info on the generated property. - /// - /// The input instance to process. - /// The instance for the current run. - /// Whether the property type supports nullability. - /// Whether MemberNotNullAttribute should be used on the setter. - private static void GetNullabilityInfo( - IFieldSymbol fieldSymbol, - SemanticModel semanticModel, - out bool isReferenceTypeOrUnconstraindTypeParameter, - out bool includeMemberNotNullOnSetAccessor) - { - // We're using IsValueType here and not IsReferenceType to also cover unconstrained type parameter cases. - // This will cover both reference types as well T when the constraints are not struct or unmanaged. - // If this is true, it means the field storage can potentially be in a null state (even if not annotated). - isReferenceTypeOrUnconstraindTypeParameter = !fieldSymbol.Type.IsValueType; - - // This is used to avoid nullability warnings when setting the property from a constructor, in case the field - // was marked as not nullable. Nullability annotations are assumed to always be enabled to make the logic simpler. - // Consider this example: - // - // partial class MyViewModel : ReactiveObject - // { - // public MyViewModel() - // { - // Name = "Bob"; - // } - // - // [ObservableAsProperty] - // private string name; - // } - // - // The [MemberNotNull] attribute is needed on the setter for the generated Name property so that when Name - // is set, the compiler can determine that the name backing field is also being set (to a non null value). - // Of course, this can only be the case if the field type is also of a type that could be in a null state. - includeMemberNotNullOnSetAccessor = - isReferenceTypeOrUnconstraindTypeParameter && - fieldSymbol.Type.NullableAnnotation != NullableAnnotation.Annotated && - semanticModel.Compilation.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.MemberNotNullAttribute"); - } - /// /// Get the generated property name for an input field. /// diff --git a/src/ReactiveUI.SourceGenerators/Reactive/Models/PropertyInfo.cs b/src/ReactiveUI.SourceGenerators/Reactive/Models/PropertyInfo.cs index 1801326..b9c80e3 100644 --- a/src/ReactiveUI.SourceGenerators/Reactive/Models/PropertyInfo.cs +++ b/src/ReactiveUI.SourceGenerators/Reactive/Models/PropertyInfo.cs @@ -4,6 +4,7 @@ // See the LICENSE file in the project root for full license information. using Microsoft.CodeAnalysis.CSharp.Syntax; +using ReactiveUI.SourceGenerators.Helpers; namespace ReactiveUI.SourceGenerators.Reactive.Models; @@ -20,8 +21,8 @@ internal sealed record PropertyInfo( string TypeNameWithNullabilityAnnotations, string FieldName, string PropertyName, - EqualsValueClauseSyntax? Initializer, // TODO: Remove Initializer when ObservableAsPropertyHelper is updated + string? Initializer, // TODO: Remove Initializer when ObservableAsPropertyHelper is updated bool IsReferenceTypeOrUnconstraindTypeParameter, bool IncludeMemberNotNullOnSetAccessor, - ForwardAttributes ForwardedAttributes, + EquatableArray ForwardedAttributes, string AccessModifier); diff --git a/src/ReactiveUI.SourceGenerators/Reactive/ReactiveGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators/Reactive/ReactiveGenerator.Execute.cs index 43fe70f..988dc1c 100644 --- a/src/ReactiveUI.SourceGenerators/Reactive/ReactiveGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators/Reactive/ReactiveGenerator.Execute.cs @@ -8,6 +8,8 @@ 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.Models; @@ -76,26 +78,93 @@ public sealed partial class ReactiveGenerator token.ThrowIfCancellationRequested(); // Get the nullability info for the property - GetNullabilityInfo( - fieldSymbol, - context.SemanticModel, - out var isReferenceTypeOrUnconstraindTypeParameter, - out var includeMemberNotNullOnSetAccessor); - - // Get the attributes for the field - var attributes = fieldSymbol.GetAttributes() - .Where(x => x.AttributeClass?.HasFullyQualifiedMetadataName(AttributeDefinitions.ReactiveAttributeType) == false) - .ToImmutableArray(); - PropertyAttributeData[] propertyAttributes = []; - if (attributes.Length > 0) + fieldSymbol.GetNullabilityInfo( + context.SemanticModel, + out var isReferenceTypeOrUnconstraindTypeParameter, + out var includeMemberNotNullOnSetAccessor); + + using var forwardedAttributes = ImmutableArrayBuilder.Rent(); + + // Gather attributes info + foreach (var attribute in fieldSymbol.GetAttributes()) + { + token.ThrowIfCancellationRequested(); + + // Track the current attribute for forwarding if it is a validation attribute + if (attribute.AttributeClass?.InheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute") == true) + { + forwardedAttributes.Add(AttributeInfo.Create(attribute)); + } + + // Track the current attribute for forwarding if it is a Json Serialization attribute + if (attributeData.AttributeClass?.InheritsFromFullyQualifiedMetadataName("System.Text.Json.Serialization.JsonAttribute") == true) + { + forwardedAttributes.Add(AttributeInfo.Create(attribute)); + } + + // Also track the current attribute for forwarding if it is of any of the following types: + if (attribute.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.UIHintAttribute") == true || + attribute.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute") == true || + attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.DisplayAttribute") == true || + attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.EditableAttribute") == true || + attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.KeyAttribute") == true || + attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.Runtime.Serialization.DataMemberAttribute") == true || + attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.Runtime.Serialization.IgnoreDataMemberAttribute") == true) + { + forwardedAttributes.Add(AttributeInfo.Create(attribute)); + } + } + + token.ThrowIfCancellationRequested(); + var fieldDeclaration = (FieldDeclarationSyntax)context.TargetNode.Parent!.Parent!; + + // Gather explicit forwarded attributes info + foreach (var attributeList in fieldDeclaration.AttributeLists) { - // Generate attribute list for fields. - propertyAttributes = attributes.GenerateAttributes( - AttributeTargets.Property, - token); + // Only look for attribute lists explicitly targeting the (generated) property. Roslyn will normally emit a + // CS0657 warning (invalid target), but that is automatically suppressed by a dedicated diagnostic suppressor + // that recognizes uses of this target specifically to support [Reactive]. + if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword)) + { + continue; + } + + token.ThrowIfCancellationRequested(); + + foreach (var attribute in attributeList.Attributes) + { + // Roslyn ignores attributes in an attribute list with an invalid target, so we can't get the AttributeData as usual. + // To reconstruct all necessary attribute info to generate the serialized model, we use the following steps: + // - We try to get the attribute symbol from the semantic model, for the current attribute syntax. In case this is not + // available (in theory it shouldn't, but it can be), we try to get it from the candidate symbols list for the node. + // If there are no candidates or more than one, we just issue a diagnostic and stop processing the current attribute. + // The returned symbols might be method symbols (constructor attribute) so in that case we can get the declaring type. + // - We then go over each attribute argument expression and get the operation for it. This will still be available even + // though the rest of the attribute is not validated nor bound at all. From the operation we can still retrieve all + // constant values to build the AttributeInfo model. After all, attributes only support constant values, typeof(T) + // expressions, or arrays of either these two types, or of other arrays with the same rules, recursively. + // - From the syntax, we can also determine the identifier names for named attribute arguments, if any. + // There is no need to validate anything here: the attribute will be forwarded as is, and then Roslyn will validate on the + // generated property. Users will get the same validation they'd have had directly over the field. The only drawback is the + // lack of IntelliSense when constructing attributes over the field, but this is the best we can do from this end anyway. + if (!context.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, context.SemanticModel, attributeArguments, token, out var attributeInfo)) + { + continue; + } + + forwardedAttributes.Add(attributeInfo); + } } - var forwardedAttributes = new ForwardAttributes(propertyAttributes); + var forwardedAttributesString = forwardedAttributes.ToImmutable().Select(x => x.ToString()).ToImmutableArray(); token.ThrowIfCancellationRequested(); // Get the containing type info @@ -116,7 +185,7 @@ public sealed partial class ReactiveGenerator null, isReferenceTypeOrUnconstraindTypeParameter, includeMemberNotNullOnSetAccessor, - forwardedAttributes, + forwardedAttributesString, accessModifier); } @@ -144,14 +213,14 @@ private static string GenerateSource(string containingTypeName, string containin namespace {{containingNamespace}} { -{{AddTabs(1)}}/// -{{AddTabs(1)}}/// Partial class for the {{containingTypeName}} which contains ReactiveUI Reactive property initialization. -{{AddTabs(1)}}/// -{{AddTabs(1)}}{{containingClassVisibility}} partial {{containingType}} {{containingTypeName}} -{{AddTabs(1)}}{ -{{AddTabs(2)}}[global::System.CodeDom.Compiler.GeneratedCode("{{GeneratorName}}", "{{GeneratorVersion}}")] + /// + /// Partial class for the {{containingTypeName}} which contains ReactiveUI Reactive property initialization. + /// + {{containingClassVisibility}} partial {{containingType}} {{containingTypeName}} + { + [global::System.CodeDom.Compiler.GeneratedCode("{{GeneratorName}}", "{{GeneratorVersion}}")] {{propertyDeclarations}} -{{AddTabs(1)}}} + } } #nullable restore #pragma warning restore @@ -176,33 +245,31 @@ private static string GetPropertySyntax(PropertyInfo propertyInfo) setModifier = string.Empty; } - var propertyAttributes = string.Join("\n\t\t", excludeFromCodeCoverage.Concat(propertyInfo.ForwardedAttributes.Attributes.Select(x => x.FormatAttributes()))); + var propertyAttributes = string.Join("\n ", excludeFromCodeCoverage.Concat(propertyInfo.ForwardedAttributes)); if (propertyInfo.IncludeMemberNotNullOnSetAccessor) { return $$""" -{{AddTabs(2)}}/// -{{AddTabs(2)}}{{propertyAttributes}} -{{AddTabs(2)}}{{propertyInfo.TargetVisibility}} {{propertyInfo.TypeNameWithNullabilityAnnotations}} {{propertyInfo.PropertyName}} -{{AddTabs(2)}}{ -{{AddTabs(3)}}get => {{propertyInfo.FieldName}}; -{{AddTabs(3)}}[global::System.Diagnostics.CodeAnalysis.MemberNotNull("{{propertyInfo.FieldName}}")] -{{AddTabs(3)}}{{setModifier}}set => this.RaiseAndSetIfChanged(ref {{propertyInfo.FieldName}}, value); -{{AddTabs(2)}}} + /// + {{propertyAttributes}} + {{propertyInfo.TargetVisibility}} {{propertyInfo.TypeNameWithNullabilityAnnotations}} {{propertyInfo.PropertyName}} + { + get => {{propertyInfo.FieldName}}; + [global::System.Diagnostics.CodeAnalysis.MemberNotNull("{{propertyInfo.FieldName}}")] + {{setModifier}}set => this.RaiseAndSetIfChanged(ref {{propertyInfo.FieldName}}, value); + } """; } return $$""" -{{AddTabs(2)}}/// -{{AddTabs(2)}}{{propertyAttributes}} -{{AddTabs(2)}}{{propertyInfo.TargetVisibility}} {{propertyInfo.TypeNameWithNullabilityAnnotations}} {{propertyInfo.PropertyName}} { get => {{propertyInfo.FieldName}}; {{setModifier}}set => this.RaiseAndSetIfChanged(ref {{propertyInfo.FieldName}}, value); } + /// + {{propertyAttributes}} + {{propertyInfo.TargetVisibility}} {{propertyInfo.TypeNameWithNullabilityAnnotations}} {{propertyInfo.PropertyName}} { get => {{propertyInfo.FieldName}}; {{setModifier}}set => this.RaiseAndSetIfChanged(ref {{propertyInfo.FieldName}}, value); } """; } - private static string AddTabs(int tabCount) => new('\t', tabCount); - /// /// Validates the containing type for a given field being annotated. /// @@ -216,46 +283,4 @@ private static bool IsTargetTypeValid(IFieldSymbol fieldSymbol) return isIObservableObject || isObservableObject || hasObservableObjectAttribute; } - - /// - /// Gets the nullability info on the generated property. - /// - /// The input instance to process. - /// The instance for the current run. - /// Whether the property type supports nullability. - /// Whether MemberNotNullAttribute should be used on the setter. - private static void GetNullabilityInfo( - IFieldSymbol fieldSymbol, - SemanticModel semanticModel, - out bool isReferenceTypeOrUnconstraindTypeParameter, - out bool includeMemberNotNullOnSetAccessor) - { - // We're using IsValueType here and not IsReferenceType to also cover unconstrained type parameter cases. - // This will cover both reference types as well T when the constraints are not struct or unmanaged. - // If this is true, it means the field storage can potentially be in a null state (even if not annotated). - isReferenceTypeOrUnconstraindTypeParameter = !fieldSymbol.Type.IsValueType; - - // This is used to avoid nullability warnings when setting the property from a constructor, in case the field - // was marked as not nullable. Nullability annotations are assumed to always be enabled to make the logic simpler. - // Consider this example: - // - // partial class MyViewModel : ReactiveObject - // { - // public MyViewModel() - // { - // Name = "Bob"; - // } - // - // [Reactive] - // private string _name; - // } - // - // The [MemberNotNull] attribute is needed on the setter for the generated Name property so that when Name - // is set, the compiler can determine that the name backing field is also being set (to a non null value). - // Of course, this can only be the case if the field type is also of a type that could be in a null state. - includeMemberNotNullOnSetAccessor = - isReferenceTypeOrUnconstraindTypeParameter && - fieldSymbol.Type.NullableAnnotation != NullableAnnotation.Annotated && - semanticModel.Compilation.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.MemberNotNullAttribute"); - } } diff --git a/src/ReactiveUI.SourceGenerators/ReactiveCommand/ReactiveCommandGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators/ReactiveCommand/ReactiveCommandGenerator.Execute.cs index e1236cf..43a0b77 100644 --- a/src/ReactiveUI.SourceGenerators/ReactiveCommand/ReactiveCommandGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators/ReactiveCommand/ReactiveCommandGenerator.Execute.cs @@ -3,7 +3,6 @@ // 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.Diagnostics.CodeAnalysis; using System.Globalization; @@ -125,14 +124,14 @@ private static string GenerateSource(string containingTypeName, string containin namespace {{containingNamespace}} { -{{AddTabs(1)}}/// -{{AddTabs(1)}}/// Partial class for the {{containingTypeName}} which contains ReactiveUI ReactiveCommand initialization. -{{AddTabs(1)}}/// -{{AddTabs(1)}}{{containingClassVisibility}} partial {{containingType}} {{containingTypeName}} -{{AddTabs(1)}}{ -{{AddTabs(2)}}[global::System.CodeDom.Compiler.GeneratedCode("{{GeneratorName}}", "{{GeneratorVersion}}")] + /// + /// Partial class for the {{containingTypeName}} which contains ReactiveUI ReactiveCommand initialization. + /// + {{containingClassVisibility}} partial {{containingType}} {{containingTypeName}} + { + [global::System.CodeDom.Compiler.GeneratedCode("{{GeneratorName}}", "{{GeneratorVersion}}")] {{propertyDeclarations}} -{{AddTabs(1)}}} + } } #nullable restore #pragma warning restore @@ -165,14 +164,14 @@ private static string GetCommandSyntax(CommandInfo commandExtensionInfo) } // Prepare any forwarded property attributes - var forwardedPropertyAttributesString = string.Join("\n\t\t", excludeFromCodeCoverage.Concat(commandExtensionInfo.ForwardedPropertyAttributes)); + var forwardedPropertyAttributesString = string.Join("\n ", excludeFromCodeCoverage.Concat(commandExtensionInfo.ForwardedPropertyAttributes)); return $$""" -{{AddTabs(2)}}private ReactiveUI.ReactiveCommand<{{inputType}}, {{outputType}}>? {{fieldName}}; + private ReactiveUI.ReactiveCommand<{{inputType}}, {{outputType}}>? {{fieldName}}; -{{AddTabs(2)}}{{forwardedPropertyAttributesString}} -{{AddTabs(2)}}public ReactiveUI.ReactiveCommand<{{inputType}}, {{outputType}}> {{commandName}} { get => {{initializer}} } + {{forwardedPropertyAttributesString}} + public ReactiveUI.ReactiveCommand<{{inputType}}, {{outputType}}> {{commandName}} { get => {{initializer}} } """; static string GenerateBasicCommand(CommandInfo commandExtensionInfo, string fieldName) @@ -480,6 +479,4 @@ private static string GetGeneratedCommandName(string methodName, bool isAsync) private static string GetGeneratedFieldName(string generatedCommandName) => $"_{char.ToLower(generatedCommandName[0], CultureInfo.InvariantCulture)}{generatedCommandName.Substring(1)}"; - - private static string AddTabs(int tabCount) => new('\t', tabCount); } diff --git a/src/ReactiveUI.SourceGenerators/ReactiveUI.SourceGenerators.csproj b/src/ReactiveUI.SourceGenerators/ReactiveUI.SourceGenerators.csproj index b25e2b5..7bd60f1 100644 --- a/src/ReactiveUI.SourceGenerators/ReactiveUI.SourceGenerators.csproj +++ b/src/ReactiveUI.SourceGenerators/ReactiveUI.SourceGenerators.csproj @@ -37,17 +37,17 @@ all build; analyzers - - + - + + diff --git a/src/ReactiveUI.SourceGenerators/RoutedControlHost/Models/RoutedControlHostInfo.cs b/src/ReactiveUI.SourceGenerators/RoutedControlHost/Models/RoutedControlHostInfo.cs index 6041f95..f959b47 100644 --- a/src/ReactiveUI.SourceGenerators/RoutedControlHost/Models/RoutedControlHostInfo.cs +++ b/src/ReactiveUI.SourceGenerators/RoutedControlHost/Models/RoutedControlHostInfo.cs @@ -3,7 +3,6 @@ // 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 ReactiveUI.SourceGenerators.Helpers; namespace ReactiveUI.SourceGenerators.Input.Models; @@ -12,8 +11,11 @@ namespace ReactiveUI.SourceGenerators.Input.Models; /// A model with gathered info on a given command method. /// internal sealed record RoutedControlHostInfo( - string ClassNamespace, - string ClassName, - string ViewModelTypeName, - TypeDeclarationSyntax DeclarationSyntax, - EquatableArray ForwardedAttributes); + string FileHintName, + string TargetName, + string TargetNamespace, + string TargetNamespaceWithNamespace, + string TargetVisibility, + string TargetType, + string BaseTypeName, + EquatableArray ForwardedAttributes); diff --git a/src/ReactiveUI.SourceGenerators/RoutedControlHost/RoutedControlHostGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators/RoutedControlHost/RoutedControlHostGenerator.Execute.cs index f3d3a33..e808378 100644 --- a/src/ReactiveUI.SourceGenerators/RoutedControlHost/RoutedControlHostGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators/RoutedControlHost/RoutedControlHostGenerator.Execute.cs @@ -4,14 +4,16 @@ // See the LICENSE file in the project root for full license information. using System; -using System.CodeDom.Compiler; -using System.IO; +using System.Collections.Immutable; +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.Input.Models; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using ReactiveUI.SourceGenerators.Models; namespace ReactiveUI.SourceGenerators.WinForms; @@ -21,243 +23,215 @@ namespace ReactiveUI.SourceGenerators.WinForms; /// public partial class RoutedControlHostGenerator { - internal static class Execute + private static readonly string GeneratorName = typeof(RoutedControlHostGenerator).FullName!; + private static readonly string GeneratorVersion = typeof(RoutedControlHostGenerator).Assembly.GetName().Version.ToString(); + + private static readonly string[] excludeFromCodeCoverage = ["[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]"]; + + private static RoutedControlHostInfo? GetClassInfo(in GeneratorAttributeSyntaxContext context, CancellationToken token) { - internal static CompilationUnitSyntax GetRoutedControlHost(RoutedControlHostInfo vmcInfo) + if (!(context.TargetNode is ClassDeclarationSyntax declaredClass && declaredClass.Modifiers.Any(SyntaxKind.PartialKeyword))) { - UsingDirectiveSyntax[] usings = - [ - UsingDirective(ParseName("ReactiveUI")) - .WithUsingKeyword( - Token( - TriviaList( - [ - Comment($"// Copyright (c) {DateTime.Now.Year} .NET Foundation and Contributors. All rights reserved."), - Comment("// Licensed to the .NET Foundation under one or more agreements."), - Comment("// The .NET Foundation licenses this file to you under the MIT license."), - Comment("// See the LICENSE file in the project root for full license information.") - ]), - SyntaxKind.UsingKeyword, - TriviaList())), - UsingDirective(ParseName("System.ComponentModel")), - UsingDirective(ParseName("System.Reactive.Disposables")), - UsingDirective(ParseName("System.Reactive.Linq")), - UsingDirective(ParseName("System.Windows.Forms")), - ]; - - var code = CompilationUnit() - .WithUsings(List(usings)) - .WithTrailingTrivia(TriviaList(CarriageReturnLineFeed)) - .WithMembers( - SingletonList( - NamespaceDeclaration(IdentifierName(vmcInfo.ClassNamespace)) - .WithLeadingTrivia(TriviaList( - Comment("// "), - Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)), - Trivia(NullableDirectiveTrivia(Token(SyntaxKind.EnableKeyword), true)))) - .WithMembers( - SingletonList( - ClassDeclaration(vmcInfo.ClassName) - .WithAttributeLists( - SingletonList( - AttributeList( - SingletonSeparatedList( - Attribute(IdentifierName("DefaultProperty")) - .WithArgumentList( - AttributeArgumentList( - SingletonSeparatedList( - AttributeArgument( - LiteralExpression( - SyntaxKind.StringLiteralExpression, - Literal("ViewModel")))))))))) - .WithModifiers( - TokenList( - [ - Token(SyntaxKind.PublicKeyword), - Token(SyntaxKind.PartialKeyword)])) - .WithBaseList( - BaseList( - SeparatedList( - new SyntaxNodeOrToken[] - { - SimpleBaseType(IdentifierName(vmcInfo.ViewModelTypeName)), - Token(SyntaxKind.CommaToken), - SimpleBaseType(IdentifierName("IReactiveObject")) - }))) - .AddAttributeLists(AttributeList(SingletonSeparatedList( - Attribute(IdentifierName(AttributeDefinitions.GeneratedCode)) - .AddArgumentListArguments( - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RoutedControlHostGenerator).FullName))), - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RoutedControlHostGenerator).Assembly.GetName().Version.ToString()))))))))))) - .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++; - writer.Indent++; - - var body = """ - private readonly CompositeDisposable _disposables = []; - private RoutingState? _router; - private Control? _defaultContent; - private IObservable? _viewContractObservable; - - /// - /// Initializes a new instance of the class. - /// - public ####REPLACEME####() - { - InitializeComponent(); + return default; + } - _disposables.Add(this.WhenAny(x => x.DefaultContent, x => x.Value).Subscribe(x => - { - if (x is not null && Controls.Count == 0) - { - Controls.Add(InitView(x)); - components?.Add(DefaultContent); - } - })); - - ViewContractObservable = Observable.Return(default(string)!); - - var vmAndContract = - this.WhenAnyObservable(x => x.Router!.CurrentViewModel!) - .CombineLatest( - this.WhenAnyObservable(x => x.ViewContractObservable!), - (vm, contract) => new { ViewModel = vm, Contract = contract }); - - Control? viewLastAdded = null; - _disposables.Add(vmAndContract.Subscribe( - x => - { - // clear all hosted controls (view or default content) - SuspendLayout(); - Controls.Clear(); - - viewLastAdded?.Dispose(); - - if (x.ViewModel is null) - { - if (DefaultContent is not null) - { - InitView(DefaultContent); - Controls.Add(DefaultContent); - } - - ResumeLayout(); - return; - } - - var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current; - var view = viewLocator.ResolveView(x.ViewModel, x.Contract); - if (view is not null) - { - view.ViewModel = x.ViewModel; - - viewLastAdded = InitView((Control)view); - } - - if (viewLastAdded is not null) - { - Controls.Add(viewLastAdded); - } - - ResumeLayout(); - }, - RxApp.DefaultExceptionHandler!.OnNext)); - } + token.ThrowIfCancellationRequested(); - /// - public event PropertyChangingEventHandler? PropertyChanging; - - /// - public event PropertyChangedEventHandler? PropertyChanged; - - /// - /// Gets or sets the default content. - /// - /// - /// The default content. - /// - [Category("ReactiveUI")] - [Description("The default control when no viewmodel is specified")] - public Control? DefaultContent - { - get => _defaultContent; - set => this.RaiseAndSetIfChanged(ref _defaultContent, value); - } + var symbol = context.TargetSymbol; + if (!symbol.TryGetAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.RoutedControlHostAttributeType, out var attributeData)) + { + return default; + } - /// - /// Gets or sets the of the view model stack. - /// - [Category("ReactiveUI")] - [Description("The router.")] - public RoutingState? Router - { - get => _router; - set => this.RaiseAndSetIfChanged(ref _router, value); - } + token.ThrowIfCancellationRequested(); - /// - /// Gets or sets the view contract observable. - /// - [Browsable(false)] - public IObservable? ViewContractObservable - { - get => _viewContractObservable; - set => this.RaiseAndSetIfChanged(ref _viewContractObservable, value); - } + var constructorArgument = attributeData.GetConstructorArguments().First(); + if (constructorArgument is not string baseTypeName) + { + return default; + } + + token.ThrowIfCancellationRequested(); + + if (symbol is not INamedTypeSymbol classSymbol) + { + return default; + } + + token.ThrowIfCancellationRequested(); - /// - /// Gets or sets the view locator. - /// - [Browsable(false)] - public IViewLocator? ViewLocator { get; set; } + var compilation = context.SemanticModel.Compilation; + var semanticModel = compilation.GetSemanticModel(context.SemanticModel.SyntaxTree); + attributeData.GatherForwardedAttributesFromClass(semanticModel, declaredClass, token, out var attributesInfo); + var classAttributesInfo = attributesInfo.Select(x => x.ToString()).ToImmutableArray(); - /// - void IReactiveObject.RaisePropertyChanging(PropertyChangingEventArgs args) => PropertyChanging?.Invoke(this, args); + token.ThrowIfCancellationRequested(); - /// - void IReactiveObject.RaisePropertyChanged(PropertyChangedEventArgs args) => PropertyChanged?.Invoke(this, args); + // Get the containing type info + var targetInfo = TargetInfo.From(classSymbol); + + token.ThrowIfCancellationRequested(); + + return new RoutedControlHostInfo( + targetInfo.FileHintName, + targetInfo.TargetName, + targetInfo.TargetNamespace, + targetInfo.TargetNamespaceWithNamespace, + targetInfo.TargetVisibility, + targetInfo.TargetType, + baseTypeName, + classAttributesInfo); + } - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) + private static string GetRoutedControlHost(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, RoutedControlHostInfo vmcInfo) + { + // Prepare any forwarded property attributes + var forwardedAttributesString = string.Join("\n ", excludeFromCodeCoverage.Concat(vmcInfo.ForwardedAttributes)); + + return +$$""" +// Copyright (c) {{DateTime.Now.Year}} .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; +using System.ComponentModel; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Windows.Forms; + +// +#pragma warning disable +#nullable enable +namespace {{containingNamespace}} +{ + {{forwardedAttributesString}} + [DefaultProperty("ViewModel")] + [global::System.CodeDom.Compiler.GeneratedCode("{{GeneratorName}}", "{{GeneratorVersion}}")] + {{containingClassVisibility}} partial class {{containingTypeName}} : {{vmcInfo.BaseTypeName}}, IReactiveObject + { + private readonly CompositeDisposable _disposables = []; + private RoutingState? _router; + private Control? _defaultContent; + private IObservable? _viewContractObservable; + + /// + /// Initializes a new instance of the class. + /// + public {{containingTypeName}}() + { + InitializeComponent(); + _disposables.Add(this.WhenAny(x => x.DefaultContent, x => x.Value).Subscribe(x => + { + if (x is not null && Controls.Count == 0) + { + Controls.Add(InitView(x)); + components?.Add(DefaultContent); + } + })); + ViewContractObservable = Observable.Return(default(string)!); + var vmAndContract = this.WhenAnyObservable(x => x.Router!.CurrentViewModel!).CombineLatest(this.WhenAnyObservable(x => x.ViewContractObservable!), (vm, contract) => new { ViewModel = vm, Contract = contract }); + Control? viewLastAdded = null; + _disposables.Add(vmAndContract.Subscribe(x => + { + // clear all hosted controls (view or default content) + SuspendLayout(); + Controls.Clear(); + viewLastAdded?.Dispose(); + if (x.ViewModel is null) { - if (disposing && components is not null) + if (DefaultContent is not null) { - components.Dispose(); - _disposables.Dispose(); + InitView(DefaultContent); + Controls.Add(DefaultContent); } - base.Dispose(disposing); + ResumeLayout(); + return; } - private static Control InitView(Control view) + var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current; + var view = viewLocator.ResolveView(x.ViewModel, x.Contract); + if (view is not null) { - view.Dock = DockStyle.Fill; - return view; + view.ViewModel = x.ViewModel; + viewLastAdded = InitView((Control)view); } - """.Replace("####REPLACEME####", vmcInfo.ClassName); - writer.WriteLine(body); - writer.Indent--; - writer.WriteLine(Token(SyntaxKind.CloseBraceToken)); - writer.Indent--; - writer.WriteLine(Token(SyntaxKind.CloseBraceToken)); - writer.WriteLine(TriviaList( - Trivia(NullableDirectiveTrivia(Token(SyntaxKind.RestoreKeyword), true)), - Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.RestoreKeyword), true))) - .NormalizeWhitespace()); - - var output = stringStream.ToString(); - return ParseCompilationUnit(output).NormalizeWhitespace(); + + if (viewLastAdded is not null) + { + Controls.Add(viewLastAdded); + } + + ResumeLayout(); + }, RxApp.DefaultExceptionHandler!.OnNext)); + } + + /// + public event PropertyChangingEventHandler? PropertyChanging; + + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// + /// Gets or sets the default content. + /// + /// + /// The default content. + /// + [Category("ReactiveUI")] + [Description("The default control when no viewmodel is specified")] + public Control? DefaultContent { get => _defaultContent; set => this.RaiseAndSetIfChanged(ref _defaultContent, value); } + + /// + /// Gets or sets the of the view model stack. + /// + [Category("ReactiveUI")] + [Description("The router.")] + public RoutingState? Router { get => _router; set => this.RaiseAndSetIfChanged(ref _router, value); } + + /// + /// Gets or sets the view contract observable. + /// + [Browsable(false)] + public IObservable? ViewContractObservable { get => _viewContractObservable; set => this.RaiseAndSetIfChanged(ref _viewContractObservable, value); } + + /// + /// Gets or sets the view locator. + /// + [Browsable(false)] + public IViewLocator? ViewLocator { get; set; } + + /// + void IReactiveObject.RaisePropertyChanging(PropertyChangingEventArgs args) => PropertyChanging?.Invoke(this, args); + + /// + void IReactiveObject.RaisePropertyChanged(PropertyChangedEventArgs args) => PropertyChanged?.Invoke(this, args); + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && components is not null) + { + components.Dispose(); + _disposables.Dispose(); + } + + base.Dispose(disposing); } + + private static Control InitView(Control view) + { + view.Dock = DockStyle.Fill; + return view; + } + } +} +#nullable restore +#pragma warning restore +"""; } } diff --git a/src/ReactiveUI.SourceGenerators/RoutedControlHost/RoutedControlHostGenerator.cs b/src/ReactiveUI.SourceGenerators/RoutedControlHost/RoutedControlHostGenerator.cs index 3b309e3..a98793e 100644 --- a/src/ReactiveUI.SourceGenerators/RoutedControlHost/RoutedControlHostGenerator.cs +++ b/src/ReactiveUI.SourceGenerators/RoutedControlHost/RoutedControlHostGenerator.cs @@ -3,20 +3,14 @@ // 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.Immutable; -using System.IO; using System.Linq; using System.Text; -using System.Threading; 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.Input.Models; -using ReactiveUI.SourceGenerators.Models; namespace ReactiveUI.SourceGenerators.WinForms; @@ -33,117 +27,41 @@ public void Initialize(IncrementalGeneratorInitializationContext context) ctx.AddSource($"{AttributeDefinitions.RoutedControlHostAttributeType}.g.cs", SourceText.From(AttributeDefinitions.GetRoutedControlHostAttribute(), Encoding.UTF8))); // Gather info for all annotated IViewFor Classes - IncrementalValuesProvider<(HierarchyInfo Hierarchy, Result Info)> rchInfoWithErrors = + var rchInfo = context.SyntaxProvider .ForAttributeWithMetadataName( AttributeDefinitions.RoutedControlHostAttributeType, static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 }, - static (context, token) => - { - token.ThrowIfCancellationRequested(); - using var hierarchys = ImmutableArrayBuilder.Rent(); - RoutedControlHostInfo rchInfo = default!; - HierarchyInfo hierarchy = default!; - - if (context.TargetNode is ClassDeclarationSyntax declaredClass && declaredClass.Modifiers.Any(SyntaxKind.PartialKeyword)) - { - token.ThrowIfCancellationRequested(); - var compilation = context.SemanticModel.Compilation; - var semanticModel = compilation.GetSemanticModel(context.SemanticModel.SyntaxTree); - var symbol = ModelExtensions.GetDeclaredSymbol(semanticModel, declaredClass, token)!; - if (symbol.TryGetAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.RoutedControlHostAttributeType, out var attributeData)) - { - token.ThrowIfCancellationRequested(); - var classSymbol = symbol as INamedTypeSymbol; - var classNamespace = classSymbol?.ContainingNamespace.ToString(); - var className = declaredClass.Identifier.ValueText; - var constructorArgument = attributeData.GetConstructorArguments().First(); - if (constructorArgument is string viewModelTypeName) - { - token.ThrowIfCancellationRequested(); - GatherForwardedAttributes(attributeData, semanticModel, declaredClass, token, out var classAttributesInfo); - token.ThrowIfCancellationRequested(); - - rchInfo = new RoutedControlHostInfo( - classNamespace!, - className, - viewModelTypeName!, - declaredClass, - classAttributesInfo); - - hierarchy = HierarchyInfo.From(classSymbol!); - } - } - } - - token.ThrowIfCancellationRequested(); - ImmutableArray diagnostics = default; - return (Hierarchy: hierarchy, new Result(rchInfo, diagnostics)); - }) - .Where(static item => item.Hierarchy is not null)!; - - ////// Output the diagnostics - ////context.ReportDiagnostics(iViewForInfoWithErrors.Select(static (item, _) => item.Info.Errors)); - - // Get the filtered sequence to enable caching - var rchInfo = - rchInfoWithErrors - .Where(static item => item.Info.Value is not null)!; + static (context, token) => GetClassInfo(context, token)) + .Where(x => x != null) + .Select((x, _) => x!) + .Collect(); // Generate the requested properties and methods for IViewFor - context.RegisterSourceOutput(rchInfo, static (context, item) => - context.AddSource($"{item.Hierarchy.FilenameHint}.RoutedControlHost.g.cs", Execute.GetRoutedControlHost(item.Info.Value))); - } - - private static void GatherForwardedAttributes( - AttributeData attributeData, - SemanticModel semanticModel, - ClassDeclarationSyntax classDeclaration, - CancellationToken token, - out ImmutableArray classAttributesInfo) - { - using var classAttributesInfoBuilder = ImmutableArrayBuilder.Rent(); - - static void GatherForwardedAttributes( - AttributeData attributeData, - SemanticModel semanticModel, - ClassDeclarationSyntax classDeclaration, - CancellationToken token, - ImmutableArrayBuilder classAttributesInfo) + context.RegisterSourceOutput(rchInfo, static (context, input) => { - // Gather explicit forwarded attributes info - foreach (var attributeList in classDeclaration.AttributeLists) - { - foreach (var attribute in attributeList.Attributes) - { - if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out var attributeTypeSymbol)) - { - continue; - } + var groupedPropertyInfo = input.GroupBy( + static info => (info.FileHintName, info.TargetName, info.TargetNamespace, info.TargetVisibility, info.TargetType), + static info => info) + .ToImmutableArray(); - var attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); - - // Try to extract the forwarded attribute - if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, token, out var attributeInfo)) - { - continue; - } + if (groupedPropertyInfo.Length == 0) + { + return; + } - var ignoreAttribute = attributeData.AttributeClass?.GetFullyQualifiedMetadataName(); - if (attributeInfo.TypeName.Contains(ignoreAttribute)) - { - continue; - } + foreach (var grouping in groupedPropertyInfo) + { + var items = grouping.ToImmutableArray(); - // Add the new attribute info to the right builder - classAttributesInfo.Add(attributeInfo); + if (items.Length == 0) + { + continue; } - } - } - // If the method is not a partial definition/implementation, just gather attributes from the method with no modifications - GatherForwardedAttributes(attributeData, semanticModel, classDeclaration, token, classAttributesInfoBuilder); - - classAttributesInfo = classAttributesInfoBuilder.ToImmutable(); + var source = GetRoutedControlHost(grouping.Key.TargetName, grouping.Key.TargetNamespace, grouping.Key.TargetVisibility, grouping.Key.TargetType, grouping.FirstOrDefault()); + context.AddSource($"{grouping.Key.FileHintName}.RoutedControlHost.g.cs", source); + } + }); } } diff --git a/src/ReactiveUI.SourceGenerators/ViewModelControlHost/Models/ViewModelControlHostInfo.cs b/src/ReactiveUI.SourceGenerators/ViewModelControlHost/Models/ViewModelControlHostInfo.cs index 548c61a..effe738 100644 --- a/src/ReactiveUI.SourceGenerators/ViewModelControlHost/Models/ViewModelControlHostInfo.cs +++ b/src/ReactiveUI.SourceGenerators/ViewModelControlHost/Models/ViewModelControlHostInfo.cs @@ -3,7 +3,6 @@ // 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 ReactiveUI.SourceGenerators.Helpers; namespace ReactiveUI.SourceGenerators.Input.Models; @@ -12,8 +11,11 @@ namespace ReactiveUI.SourceGenerators.Input.Models; /// A model with gathered info on a given command method. /// internal sealed record ViewModelControlHostInfo( - string ClassNamespace, - string ClassName, + string FileHintName, + string TargetName, + string TargetNamespace, + string TargetNamespaceWithNamespace, + string TargetVisibility, + string TargetType, string ViewModelTypeName, - TypeDeclarationSyntax DeclarationSyntax, - EquatableArray ForwardedAttributes); + EquatableArray ForwardedAttributes); diff --git a/src/ReactiveUI.SourceGenerators/ViewModelControlHost/ViewModelControlHostGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators/ViewModelControlHost/ViewModelControlHostGenerator.Execute.cs index eaacf35..a362f9a 100644 --- a/src/ReactiveUI.SourceGenerators/ViewModelControlHost/ViewModelControlHostGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators/ViewModelControlHost/ViewModelControlHostGenerator.Execute.cs @@ -4,14 +4,16 @@ // See the LICENSE file in the project root for full license information. using System; -using System.CodeDom.Compiler; -using System.IO; +using System.Collections.Immutable; +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.Input.Models; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using ReactiveUI.SourceGenerators.Models; namespace ReactiveUI.SourceGenerators.WinForms; @@ -21,304 +23,254 @@ namespace ReactiveUI.SourceGenerators.WinForms; /// public partial class ViewModelControlHostGenerator { - internal static class Execute + private static readonly string GeneratorName = typeof(ViewModelControlHostGenerator).FullName!; + private static readonly string GeneratorVersion = typeof(ViewModelControlHostGenerator).Assembly.GetName().Version.ToString(); + + private static readonly string[] excludeFromCodeCoverage = ["[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]"]; + + private static ViewModelControlHostInfo? GetClassInfo(in GeneratorAttributeSyntaxContext context, CancellationToken token) { - internal static CompilationUnitSyntax GetViewModelControlHost(ViewModelControlHostInfo vmcInfo) + if (!(context.TargetNode is ClassDeclarationSyntax declaredClass && declaredClass.Modifiers.Any(SyntaxKind.PartialKeyword))) { - UsingDirectiveSyntax[] usings = - [ - UsingDirective(ParseName("ReactiveUI")) - .WithUsingKeyword( - Token( - TriviaList( - [ - Comment($"// Copyright (c) {DateTime.Now.Year} .NET Foundation and Contributors. All rights reserved."), - Comment("// Licensed to the .NET Foundation under one or more agreements."), - Comment("// The .NET Foundation licenses this file to you under the MIT license."), - Comment("// See the LICENSE file in the project root for full license information.") - ]), - SyntaxKind.UsingKeyword, - TriviaList())), - UsingDirective(ParseName("System.ComponentModel")), - UsingDirective(ParseName("System.Reactive.Disposables")), - UsingDirective(ParseName("System.Reactive.Linq")), - UsingDirective(ParseName("System.Windows.Forms")), - ]; - - var code = CompilationUnit() - .WithUsings(List(usings)) - .WithTrailingTrivia(TriviaList(CarriageReturnLineFeed)) - .WithMembers( - SingletonList( - NamespaceDeclaration(IdentifierName(vmcInfo.ClassNamespace)) - .WithLeadingTrivia(TriviaList( - Comment("// "), - Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)), - Trivia(NullableDirectiveTrivia(Token(SyntaxKind.EnableKeyword), true)))) - .WithMembers( - SingletonList( - ClassDeclaration(vmcInfo.ClassName) - .WithAttributeLists( - SingletonList( - AttributeList( - SingletonSeparatedList( - Attribute(IdentifierName("DefaultProperty")) - .WithArgumentList( - AttributeArgumentList( - SingletonSeparatedList( - AttributeArgument( - LiteralExpression( - SyntaxKind.StringLiteralExpression, - Literal("ViewModel")))))))))) - .WithModifiers( - TokenList( - [ - Token(SyntaxKind.PublicKeyword), - Token(SyntaxKind.PartialKeyword)])) - .WithBaseList( - BaseList( - SeparatedList( - new SyntaxNodeOrToken[] - { - SimpleBaseType(IdentifierName(vmcInfo.ViewModelTypeName)), - Token(SyntaxKind.CommaToken), - SimpleBaseType(IdentifierName("IReactiveObject")), - Token(SyntaxKind.CommaToken), - SimpleBaseType(IdentifierName("IViewFor")) - }))) - .AddAttributeLists(AttributeList(SingletonSeparatedList( - Attribute(IdentifierName(AttributeDefinitions.GeneratedCode)) - .AddArgumentListArguments( - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ViewModelControlHostGenerator).FullName))), - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ViewModelControlHostGenerator).Assembly.GetName().Version.ToString()))))))))))) - .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++; - writer.Indent++; - - var body = """ - private readonly CompositeDisposable _disposables = []; - private Control? _defaultContent; - private IObservable? _viewContractObservable; - private object? _viewModel; - private object? _content; - private bool _cacheViews; - - /// - /// Initializes a new instance of the class. - /// - public ####REPLACEME####() - { - InitializeComponent(); - _cacheViews = DefaultCacheViewsEnabled; - foreach (var d in SetupBindings()) - { - _disposables.Add(d); - } - } + return default; + } - /// - public event PropertyChangingEventHandler? PropertyChanging; + token.ThrowIfCancellationRequested(); + var symbol = context.TargetSymbol; + if (!symbol.TryGetAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.ViewModelControlHostAttributeType, out var attributeData)) + { + return default; + } - /// - public event PropertyChangedEventHandler? PropertyChanged; + token.ThrowIfCancellationRequested(); + if (symbol is not INamedTypeSymbol classSymbol) + { + return default; + } - /// - /// Gets or sets a value indicating whether [default cache views enabled]. - /// - public static bool DefaultCacheViewsEnabled { get; set; } + var constructorArgument = attributeData.GetConstructorArguments().First(); + if (constructorArgument is not string viewModelTypeName) + { + return default; + } - /// - /// Gets the current view. - /// - public Control? CurrentView => _content as Control; + token.ThrowIfCancellationRequested(); + var compilation = context.SemanticModel.Compilation; + var semanticModel = compilation.GetSemanticModel(context.SemanticModel.SyntaxTree); + attributeData.GatherForwardedAttributesFromClass(semanticModel, declaredClass, token, out var attributesInfo); + var classAttributesInfo = attributesInfo.Select(x => x.ToString()).ToImmutableArray(); + + token.ThrowIfCancellationRequested(); + + // Get the containing type info + var targetInfo = TargetInfo.From(classSymbol); + + return new ViewModelControlHostInfo( + targetInfo.FileHintName, + targetInfo.TargetName, + targetInfo.TargetNamespace, + targetInfo.TargetNamespaceWithNamespace, + targetInfo.TargetVisibility, + targetInfo.TargetType, + viewModelTypeName!, + classAttributesInfo); + } - /// - /// Gets or sets the default content. - /// - [Category("ReactiveUI")] - [Description("The default control when no viewmodel is specified")] - public Control? DefaultContent - { - get => _defaultContent; - set => this.RaiseAndSetIfChanged(ref _defaultContent, value); - } + private static string GetViewModelControlHost(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, ViewModelControlHostInfo vmcInfo) + { + // Prepare any forwarded property attributes + var forwardedAttributesString = string.Join("\n ", excludeFromCodeCoverage.Concat(vmcInfo.ForwardedAttributes)); - /// - /// Gets or sets the view contract observable. - /// - /// - /// The view contract observable. - /// - [Browsable(false)] - public IObservable? ViewContractObservable - { - get => _viewContractObservable; - set => this.RaiseAndSetIfChanged(ref _viewContractObservable, value); - } + return +$$""" +// Copyright (c) {{DateTime.Now.Year}} .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; +using System.ComponentModel; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Windows.Forms; + +// +#pragma warning disable +#nullable enable +namespace {{containingNamespace}} +{ + {{forwardedAttributesString}} + [DefaultProperty("ViewModel")] + [global::System.CodeDom.Compiler.GeneratedCode("{{GeneratorName}}", "{{GeneratorVersion}}")] + public partial class {{containingTypeName}} : {{vmcInfo.ViewModelTypeName}}, IReactiveObject, IViewFor + { + private readonly CompositeDisposable _disposables = []; + private Control? _defaultContent; + private IObservable? _viewContractObservable; + private object? _viewModel; + private object? _content; + private bool _cacheViews; + + /// + /// Initializes a new instance of the class. + /// + public {{containingTypeName}}() + { + InitializeComponent(); + _cacheViews = DefaultCacheViewsEnabled; + foreach (var d in SetupBindings()) + { + _disposables.Add(d); + } + } - /// - /// Gets or sets the view locator. - /// - [Browsable(false)] - public IViewLocator? ViewLocator { get; set; } - - /// - [Category("ReactiveUI")] - [Description("The viewmodel to host.")] - [Bindable(true)] - public object? ViewModel - { - get => _viewModel; - set => this.RaiseAndSetIfChanged(ref _viewModel, value); - } + /// + public event PropertyChangingEventHandler? PropertyChanging; + + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// + /// Gets or sets a value indicating whether [default cache views enabled]. + /// + public static bool DefaultCacheViewsEnabled { get; set; } + + /// + /// Gets the current view. + /// + public Control? CurrentView => _content as Control; + + /// + /// Gets or sets the default content. + /// + [Category("ReactiveUI")] + [Description("The default control when no viewmodel is specified")] + public Control? DefaultContent { get => _defaultContent; set => this.RaiseAndSetIfChanged(ref _defaultContent, value); } + + /// + /// Gets or sets the view contract observable. + /// + /// + /// The view contract observable. + /// + [Browsable(false)] + public IObservable? ViewContractObservable { get => _viewContractObservable; set => this.RaiseAndSetIfChanged(ref _viewContractObservable, value); } + + /// + /// Gets or sets the view locator. + /// + [Browsable(false)] + public IViewLocator? ViewLocator { get; set; } + + /// + [Category("ReactiveUI")] + [Description("The viewmodel to host.")] + [Bindable(true)] + public object? ViewModel { get => _viewModel; set => this.RaiseAndSetIfChanged(ref _viewModel, value); } + + /// + /// Gets or sets the content. + /// + [Category("ReactiveUI")] + [Description("The Current View")] + [Bindable(true)] + public object? Content { get => _content; protected set => this.RaiseAndSetIfChanged(ref _content, value); } + + /// + /// Gets or sets a value indicating whether to cache views. + /// + [Category("ReactiveUI")] + [Description("Cache Views")] + [Bindable(true)] + [DefaultValue(true)] + public bool CacheViews { get => _cacheViews; set => this.RaiseAndSetIfChanged(ref _cacheViews, value); } + + /// + void IReactiveObject.RaisePropertyChanging(PropertyChangingEventArgs args) => PropertyChanging?.Invoke(this, args); + + /// + void IReactiveObject.RaisePropertyChanged(PropertyChangedEventArgs args) => PropertyChanged?.Invoke(this, args); + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && components is not null) + { + components.Dispose(); + _disposables.Dispose(); + } + + base.Dispose(disposing); + } - /// - /// Gets or sets the content. - /// - [Category("ReactiveUI")] - [Description("The Current View")] - [Bindable(true)] - public object? Content + private IEnumerable SetupBindings() + { + var viewChanges = this.WhenAnyValue(x => x!.Content).WhereNotNull().OfType().Subscribe(x => + { + // change the view in the ui + SuspendLayout(); + // clear out existing visible control view + foreach (Control? c in Controls) { - get => _content; - protected set => this.RaiseAndSetIfChanged(ref _content, value); + c?.Dispose(); + Controls.Remove(c); } - /// - /// Gets or sets a value indicating whether to cache views. - /// - [Category("ReactiveUI")] - [Description("Cache Views")] - [Bindable(true)] - [DefaultValue(true)] - public bool CacheViews + x!.Dock = DockStyle.Fill; + Controls.Add(x); + ResumeLayout(); + }); + yield return viewChanges!; + yield return this.WhenAnyValue(x => x.DefaultContent).Subscribe(x => + { + if (x is not null) { - get => _cacheViews; - set => this.RaiseAndSetIfChanged(ref _cacheViews, value); + Content = DefaultContent; } - - /// - void IReactiveObject.RaisePropertyChanging(PropertyChangingEventArgs args) => PropertyChanging?.Invoke(this, args); - - /// - void IReactiveObject.RaisePropertyChanged(PropertyChangedEventArgs args) => PropertyChanged?.Invoke(this, args); - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) + }); + ViewContractObservable = Observable.Return(string.Empty); + var vmAndContract = this.WhenAnyValue(x => x.ViewModel).CombineLatest(this.WhenAnyObservable(x => x.ViewContractObservable!), (vm, contract) => new { ViewModel = vm, Contract = contract }); + yield return vmAndContract.Subscribe(x => + { + // set content to default when viewmodel is null + if (ViewModel is null) { - if (disposing && components is not null) + if (DefaultContent is not null) { - components.Dispose(); - _disposables.Dispose(); + Content = DefaultContent; } - base.Dispose(disposing); + return; } - private IEnumerable SetupBindings() + if (CacheViews) { - var viewChanges = - this.WhenAnyValue(x => x!.Content) - .WhereNotNull() - .OfType() - .Subscribe(x => - { - // change the view in the ui - SuspendLayout(); - - // clear out existing visible control view - foreach (Control? c in Controls) - { - c?.Dispose(); - Controls.Remove(c); - } - - x!.Dock = DockStyle.Fill; - Controls.Add(x); - ResumeLayout(); - }); - - yield return viewChanges!; - - yield return this.WhenAnyValue(x => x.DefaultContent).Subscribe(x => + // when caching views, check the current viewmodel and type + var c = _content as IViewFor; + if (c?.ViewModel is not null && c.ViewModel.GetType() == x.ViewModel!.GetType()) { - if (x is not null) - { - Content = DefaultContent; - } - }); - - ViewContractObservable = Observable.Return(string.Empty); - - var vmAndContract = - this.WhenAnyValue(x => x.ViewModel) - .CombineLatest( - this.WhenAnyObservable(x => x.ViewContractObservable!), - (vm, contract) => new { ViewModel = vm, Contract = contract }); - - yield return vmAndContract.Subscribe( - x => - { - // set content to default when viewmodel is null - if (ViewModel is null) - { - if (DefaultContent is not null) - { - Content = DefaultContent; - } - - return; - } - - if (CacheViews) - { - // when caching views, check the current viewmodel and type - var c = _content as IViewFor; - - if (c?.ViewModel is not null && c.ViewModel.GetType() == x.ViewModel!.GetType()) - { - c.ViewModel = x.ViewModel; - - // return early here after setting the viewmodel - // allowing the view to update it's bindings - return; - } - } - - var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current; - var view = viewLocator.ResolveView(x.ViewModel, x.Contract); - if (view is not null) - { - view.ViewModel = x.ViewModel; - Content = view; - } - }, - RxApp.DefaultExceptionHandler!.OnNext); + c.ViewModel = x.ViewModel; + // return early here after setting the viewmodel + // allowing the view to update it's bindings + return; + } } - """.Replace("####REPLACEME####", vmcInfo.ClassName); - writer.WriteLine(body); - writer.Indent--; - writer.WriteLine(Token(SyntaxKind.CloseBraceToken)); - writer.Indent--; - writer.WriteLine(Token(SyntaxKind.CloseBraceToken)); - writer.WriteLine(TriviaList( - Trivia(NullableDirectiveTrivia(Token(SyntaxKind.RestoreKeyword), true)), - Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.RestoreKeyword), true))) - .NormalizeWhitespace()); - - var output = stringStream.ToString(); - return ParseCompilationUnit(output).NormalizeWhitespace(); + + var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current; + var view = viewLocator.ResolveView(x.ViewModel, x.Contract); + if (view is not null) + { + view.ViewModel = x.ViewModel; + Content = view; + } + }, RxApp.DefaultExceptionHandler!.OnNext); } } } +#nullable restore +#pragma warning restore +"""; + } +} diff --git a/src/ReactiveUI.SourceGenerators/ViewModelControlHost/ViewModelControlHostGenerator.cs b/src/ReactiveUI.SourceGenerators/ViewModelControlHost/ViewModelControlHostGenerator.cs index d4035a1..c590d14 100644 --- a/src/ReactiveUI.SourceGenerators/ViewModelControlHost/ViewModelControlHostGenerator.cs +++ b/src/ReactiveUI.SourceGenerators/ViewModelControlHost/ViewModelControlHostGenerator.cs @@ -3,20 +3,14 @@ // 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.Immutable; -using System.IO; using System.Linq; using System.Text; -using System.Threading; 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.Input.Models; -using ReactiveUI.SourceGenerators.Models; namespace ReactiveUI.SourceGenerators.WinForms; @@ -33,117 +27,41 @@ public void Initialize(IncrementalGeneratorInitializationContext context) ctx.AddSource($"{AttributeDefinitions.ViewModelControlHostAttributeType}.g.cs", SourceText.From(AttributeDefinitions.ViewModelControlHostAttribute, Encoding.UTF8))); // Gather info for all annotated IViewFor Classes - IncrementalValuesProvider<(HierarchyInfo Hierarchy, Result Info)> vmcInfoWithErrors = + var vmcInfo = context.SyntaxProvider .ForAttributeWithMetadataName( AttributeDefinitions.ViewModelControlHostAttributeType, static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 }, - static (context, token) => - { - token.ThrowIfCancellationRequested(); - using var hierarchys = ImmutableArrayBuilder.Rent(); - ViewModelControlHostInfo vmcInfo = default!; - HierarchyInfo hierarchy = default!; - - if (context.TargetNode is ClassDeclarationSyntax declaredClass && declaredClass.Modifiers.Any(SyntaxKind.PartialKeyword)) - { - token.ThrowIfCancellationRequested(); - var compilation = context.SemanticModel.Compilation; - var semanticModel = compilation.GetSemanticModel(context.SemanticModel.SyntaxTree); - var symbol = ModelExtensions.GetDeclaredSymbol(semanticModel, declaredClass, token)!; - if (symbol.TryGetAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.ViewModelControlHostAttributeType, out var attributeData)) - { - token.ThrowIfCancellationRequested(); - var classSymbol = symbol as INamedTypeSymbol; - var classNamespace = classSymbol?.ContainingNamespace.ToString(); - var className = declaredClass.Identifier.ValueText; - var constructorArgument = attributeData.GetConstructorArguments().First(); - if (constructorArgument is string viewModelTypeName) - { - token.ThrowIfCancellationRequested(); - GatherForwardedAttributes(attributeData, semanticModel, declaredClass, token, out var classAttributesInfo); - token.ThrowIfCancellationRequested(); - - vmcInfo = new ViewModelControlHostInfo( - classNamespace!, - className, - viewModelTypeName!, - declaredClass, - classAttributesInfo); - - hierarchy = HierarchyInfo.From(classSymbol!); - } - } - } - - token.ThrowIfCancellationRequested(); - ImmutableArray diagnostics = default; - return (Hierarchy: hierarchy, new Result(vmcInfo, diagnostics)); - }) - .Where(static item => item.Hierarchy is not null)!; - - ////// Output the diagnostics - ////context.ReportDiagnostics(iViewForInfoWithErrors.Select(static (item, _) => item.Info.Errors)); - - // Get the filtered sequence to enable caching - var vmcInfo = - vmcInfoWithErrors - .Where(static item => item.Info.Value is not null)!; + static (context, token) => GetClassInfo(context, token)) + .Where(x => x != null) + .Select((x, _) => x!) + .Collect(); // Generate the requested properties and methods for IViewFor - context.RegisterSourceOutput(vmcInfo, static (context, item) => - context.AddSource($"{item.Hierarchy.FilenameHint}.ViewModelControlHost.g.cs", Execute.GetViewModelControlHost(item.Info.Value))); - } - - private static void GatherForwardedAttributes( - AttributeData attributeData, - SemanticModel semanticModel, - ClassDeclarationSyntax classDeclaration, - CancellationToken token, - out ImmutableArray classAttributesInfo) - { - using var classAttributesInfoBuilder = ImmutableArrayBuilder.Rent(); - - static void GatherForwardedAttributes( - AttributeData attributeData, - SemanticModel semanticModel, - ClassDeclarationSyntax classDeclaration, - CancellationToken token, - ImmutableArrayBuilder classAttributesInfo) + context.RegisterSourceOutput(vmcInfo, static (context, input) => { - // Gather explicit forwarded attributes info - foreach (var attributeList in classDeclaration.AttributeLists) - { - foreach (var attribute in attributeList.Attributes) - { - if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out var attributeTypeSymbol)) - { - continue; - } + var groupedPropertyInfo = input.GroupBy( + static info => (info.FileHintName, info.TargetName, info.TargetNamespace, info.TargetVisibility, info.TargetType), + static info => info) + .ToImmutableArray(); - var attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); - - // Try to extract the forwarded attribute - if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, token, out var attributeInfo)) - { - continue; - } + if (groupedPropertyInfo.Length == 0) + { + return; + } - var ignoreAttribute = attributeData.AttributeClass?.GetFullyQualifiedMetadataName(); - if (attributeInfo.TypeName.Contains(ignoreAttribute)) - { - continue; - } + foreach (var grouping in groupedPropertyInfo) + { + var items = grouping.ToImmutableArray(); - // Add the new attribute info to the right builder - classAttributesInfo.Add(attributeInfo); + if (items.Length == 0) + { + continue; } - } - } - // If the method is not a partial definition/implementation, just gather attributes from the method with no modifications - GatherForwardedAttributes(attributeData, semanticModel, classDeclaration, token, classAttributesInfoBuilder); - - classAttributesInfo = classAttributesInfoBuilder.ToImmutable(); + var source = GetViewModelControlHost(grouping.Key.TargetName, grouping.Key.TargetNamespace, grouping.Key.TargetVisibility, grouping.Key.TargetType, grouping.FirstOrDefault()); + context.AddSource($"{grouping.Key.FileHintName}.ViewModelControlHost.g.cs", source); + } + }); } }