From 7ee7f180eff179fd2124a636b096e41c2c79fe2a Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sun, 3 Nov 2024 14:49:39 +0000 Subject: [PATCH] Update OAPH From Field Generator (#102) --- ...stVM.ObservableAsProperties.g.verified.cs} | 12 +- ...stVM.ObservableAsProperties.g.verified.cs} | 12 +- ...stVM.ObservableAsProperties.g.verified.cs} | 12 +- .../TestViewModel.cs | 7 + .../AttributeDefinitions.cs | 3 +- .../IViewFor/IViewForGenerator.Execute.cs | 6 +- ...leAsPropertyGenerator.Generator.Execute.cs | 346 ------------------ ...ObservableAsPropertyGenerator.Generator.cs | 87 ----- .../ObservableAsPropertyGenerator.cs | 7 +- ...eAsPropertyGenerator{FromField}.Execute.cs | 267 ++++++++++++++ ...bservableAsPropertyGenerator{FromField}.cs | 59 +++ ...pertyGenerator{FromObservable}.Execute.cs} | 4 +- ...bleAsPropertyGenerator{FromObservable}.cs} | 2 +- .../Reactive/ReactiveGenerator.Execute.cs | 4 +- .../ReactiveCommandGenerator.Execute.cs | 3 +- .../RoutedControlHostGenerator.Execute.cs | 6 +- .../ViewModelControlHostGenerator.Execute.cs | 6 +- 17 files changed, 373 insertions(+), 470 deletions(-) rename src/ReactiveUI.SourceGenerator.Tests/OAPH/{OAPGeneratorTests.FromField#TestNs.TestVM.ObservableAsProperties.g.verified.cs => OAPGeneratorTests.FromField#TestVM.ObservableAsProperties.g.verified.cs} (72%) rename src/ReactiveUI.SourceGenerator.Tests/OAPH/{OAPGeneratorTests.NamedFromField#TestNs.TestVM.ObservableAsProperties.g.verified.cs => OAPGeneratorTests.NamedFromField#TestVM.ObservableAsProperties.g.verified.cs} (72%) rename src/ReactiveUI.SourceGenerator.Tests/OAPH/{OAPGeneratorTests.NonReadOnlyFromField#TestNs.TestVM.ObservableAsProperties.g.verified.cs => OAPGeneratorTests.NonReadOnlyFromField#TestVM.ObservableAsProperties.g.verified.cs} (72%) delete mode 100644 src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Generator.Execute.cs delete mode 100644 src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Generator.cs create mode 100644 src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.Execute.cs create mode 100644 src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.cs rename src/ReactiveUI.SourceGenerators/ObservableAsProperty/{ObservableAsPropertyGenerator.FromObservableGenerator.Execute.cs => ObservableAsPropertyGenerator{FromObservable}.Execute.cs} (98%) rename src/ReactiveUI.SourceGenerators/ObservableAsProperty/{ObservableAsPropertyGenerator.FromObservableGenerator.cs => ObservableAsPropertyGenerator{FromObservable}.cs} (99%) diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.FromField#TestNs.TestVM.ObservableAsProperties.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.FromField#TestVM.ObservableAsProperties.g.verified.cs similarity index 72% rename from src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.FromField#TestNs.TestVM.ObservableAsProperties.g.verified.cs rename to src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.FromField#TestVM.ObservableAsProperties.g.verified.cs index 6692b2a..b748b5e 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.FromField#TestNs.TestVM.ObservableAsProperties.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.FromField#TestVM.ObservableAsProperties.g.verified.cs @@ -1,18 +1,20 @@ -//HintName: TestNs.TestVM.ObservableAsProperties.g.cs +//HintName: TestVM.ObservableAsProperties.g.cs // #pragma warning disable #nullable enable namespace TestNs { /// - partial class TestVM + public partial class TestVM { - /// [global::System.CodeDom.Compiler.GeneratedCode("ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator", "1.1.0.0")] + /// private readonly ReactiveUI.ObservableAsPropertyHelper _test1Helper; + /// - [global::System.CodeDom.Compiler.GeneratedCode("ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator", "1.1.0.0")] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public int Test1 { get => _test1 = _test1Helper?.Value ?? _test1; } } -} \ No newline at end of file +} +#nullable restore +#pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.NamedFromField#TestNs.TestVM.ObservableAsProperties.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.NamedFromField#TestVM.ObservableAsProperties.g.verified.cs similarity index 72% rename from src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.NamedFromField#TestNs.TestVM.ObservableAsProperties.g.verified.cs rename to src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.NamedFromField#TestVM.ObservableAsProperties.g.verified.cs index 5795a76..6d8ed38 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.NamedFromField#TestNs.TestVM.ObservableAsProperties.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.NamedFromField#TestVM.ObservableAsProperties.g.verified.cs @@ -1,18 +1,20 @@ -//HintName: TestNs.TestVM.ObservableAsProperties.g.cs +//HintName: TestVM.ObservableAsProperties.g.cs // #pragma warning disable #nullable enable namespace TestNs { /// - partial class TestVM + public partial class TestVM { - /// [global::System.CodeDom.Compiler.GeneratedCode("ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator", "1.1.0.0")] + /// private readonly ReactiveUI.ObservableAsPropertyHelper _test2Helper; + /// - [global::System.CodeDom.Compiler.GeneratedCode("ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator", "1.1.0.0")] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public int Test2 { get => _test2 = _test2Helper?.Value ?? _test2; } } -} \ No newline at end of file +} +#nullable restore +#pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.NonReadOnlyFromField#TestNs.TestVM.ObservableAsProperties.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.NonReadOnlyFromField#TestVM.ObservableAsProperties.g.verified.cs similarity index 72% rename from src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.NonReadOnlyFromField#TestNs.TestVM.ObservableAsProperties.g.verified.cs rename to src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.NonReadOnlyFromField#TestVM.ObservableAsProperties.g.verified.cs index 5795a76..6d8ed38 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.NonReadOnlyFromField#TestNs.TestVM.ObservableAsProperties.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.NonReadOnlyFromField#TestVM.ObservableAsProperties.g.verified.cs @@ -1,18 +1,20 @@ -//HintName: TestNs.TestVM.ObservableAsProperties.g.cs +//HintName: TestVM.ObservableAsProperties.g.cs // #pragma warning disable #nullable enable namespace TestNs { /// - partial class TestVM + public partial class TestVM { - /// [global::System.CodeDom.Compiler.GeneratedCode("ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator", "1.1.0.0")] + /// private readonly ReactiveUI.ObservableAsPropertyHelper _test2Helper; + /// - [global::System.CodeDom.Compiler.GeneratedCode("ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator", "1.1.0.0")] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public int Test2 { get => _test2 = _test2Helper?.Value ?? _test2; } } -} \ No newline at end of file +} +#nullable restore +#pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs index 72e7230..7eb9857 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs @@ -35,6 +35,9 @@ public partial class TestViewModel : ReactiveObject, IActivatableViewModel, IDis [ObservableAsProperty(ReadOnly = false)] private double? _test11Property = 11.1d; + [ObservableAsProperty(ReadOnly = false)] + private double _test13Property = 11.1d; + [property: Test(AParameter = "Test Input")] [Reactive] private double? _test12Property = 12.1d; @@ -138,6 +141,10 @@ public TestViewModel() Console.Out.WriteLine(_myReadOnlyNonNullProperty); _testNonNullSubject.OnNext(default); + Console.Out.WriteLine(_test13Property); + Console.Out.WriteLine(Test13Property); + Console.Out.WriteLine(_test13PropertyHelper); + // expected value 0 as the _testNonNullSubject has been updated. Console.Out.WriteLine(MyReadOnlyNonNullProperty); Console.Out.WriteLine(_myReadOnlyNonNullProperty); diff --git a/src/ReactiveUI.SourceGenerators/AttributeDefinitions.cs b/src/ReactiveUI.SourceGenerators/AttributeDefinitions.cs index 9919884..77a069c 100644 --- a/src/ReactiveUI.SourceGenerators/AttributeDefinitions.cs +++ b/src/ReactiveUI.SourceGenerators/AttributeDefinitions.cs @@ -12,10 +12,11 @@ namespace ReactiveUI.SourceGenerators.Helpers; internal static class AttributeDefinitions { public const string GeneratedCode = "global::System.CodeDom.Compiler.GeneratedCode"; - public const string ExcludeFromCodeCoverage = "global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"; public const string Obsolete = "global::System.Obsolete"; public const string AccessModifierType = "ReactiveUI.SourceGenerators.AccessModifier"; + public static string[] ExcludeFromCodeCoverage = ["[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]"]; + public static string ExcludeFromCodeCoverageString = "global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"; public static string GetAccessModifierEnum() => $$""" // Copyright (c) {{DateTime.Now.Year}} .NET Foundation and Contributors. All rights reserved. diff --git a/src/ReactiveUI.SourceGenerators/IViewFor/IViewForGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators/IViewFor/IViewForGenerator.Execute.cs index ab1a640..03c8e1a 100644 --- a/src/ReactiveUI.SourceGenerators/IViewFor/IViewForGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators/IViewFor/IViewForGenerator.Execute.cs @@ -189,7 +189,7 @@ namespace {{containingNamespace}} /// Partial class for the {{containingTypeName}} which contains ReactiveUI IViewFor initialization. /// {{forwardedAttributesString}} - partial class {{containingTypeName}} : IViewFor<{{iviewForInfo.ViewModelTypeName}}> + {{containingClassVisibility}} partial {{containingType}} {{containingTypeName}} : IViewFor<{{iviewForInfo.ViewModelTypeName}}> { /// [Category("ReactiveUI")] @@ -223,7 +223,7 @@ namespace {{containingNamespace}} /// Partial class for the {{containingTypeName}} which contains ReactiveUI IViewFor initialization. /// {{forwardedAttributesString}} - public partial class {{containingTypeName}} : IViewFor<{{iviewForInfo.ViewModelTypeName}}> + {{containingClassVisibility}} partial {{containingType}} {{containingTypeName}} : IViewFor<{{iviewForInfo.ViewModelTypeName}}> { /// /// The view model dependency property. @@ -282,7 +282,7 @@ namespace {{containingNamespace}} /// Partial class for the {{containingTypeName}} which contains ReactiveUI IViewFor initialization. /// {{forwardedAttributesString}} - public partial class {{containingTypeName}} : IViewFor<{{iviewForInfo.ViewModelTypeName}}> + {{containingClassVisibility}} partial {{containingType}} {{containingTypeName}} : IViewFor<{{iviewForInfo.ViewModelTypeName}}> { public static readonly BindableProperty ViewModelProperty = BindableProperty.Create(nameof(ViewModel), typeof({{iviewForInfo.ViewModelTypeName}}), typeof(IViewFor<{{iviewForInfo.ViewModelTypeName}}>), default({{iviewForInfo.ViewModelTypeName}}), BindingMode.OneWay, propertyChanged: OnViewModelChanged); diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Generator.Execute.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Generator.Execute.cs deleted file mode 100644 index 88acd08..0000000 --- a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Generator.Execute.cs +++ /dev/null @@ -1,346 +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.Generic; -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -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.Models; -using ReactiveUI.SourceGenerators.Reactive.Models; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; -using static ReactiveUI.SourceGenerators.Diagnostics.DiagnosticDescriptors; - -namespace ReactiveUI.SourceGenerators; - -/// -/// ReactiveGenerator. -/// -/// -public sealed partial class ObservableAsPropertyGenerator -{ - /// - /// A container for all the logic for . - /// - internal static partial class Execute - { - /// - /// Gets the instance for the input field. - /// - /// The input instance to process. - /// The generated instance for . - internal static ImmutableArray GetPropertySyntax(PropertyInfo propertyInfo) - { - // Get the property type syntax - TypeSyntax propertyType = IdentifierName(propertyInfo.TypeNameWithNullabilityAnnotations); - - string getterFieldIdentifierName; - - // In case the backing field is exactly named "value", we need to add the "this." prefix to ensure that comparisons and assignments - // with it in the generated setter body are executed correctly and without conflicts with the implicit value parameter. - if (propertyInfo.FieldName == "value") - { - // We only need to add "this." when referencing the field in the setter (getter and XML docs are not ambiguous) - getterFieldIdentifierName = "value"; - } - else if (SyntaxFacts.GetKeywordKind(propertyInfo.FieldName) != SyntaxKind.None || - SyntaxFacts.GetContextualKeywordKind(propertyInfo.FieldName) != SyntaxKind.None) - { - // If the identifier for the field could potentially be a keyword, we must escape it. - // This usually happens if the annotated field was escaped as well (eg. "@event"). - // In this case, we must always escape the identifier, in all cases. - getterFieldIdentifierName = $"@{propertyInfo.FieldName}"; - } - else - { - getterFieldIdentifierName = propertyInfo.FieldName; - } - - ArrowExpressionClauseSyntax getterArrowExpression; - - if (propertyInfo.TypeNameWithNullabilityAnnotations.EndsWith("?")) - { - getterArrowExpression = ArrowExpressionClause(ParseExpression($"{getterFieldIdentifierName} = ({getterFieldIdentifierName}Helper == null ? {getterFieldIdentifierName} : {getterFieldIdentifierName}Helper.Value)")); - } - else - { - getterArrowExpression = ArrowExpressionClause(ParseExpression($"{getterFieldIdentifierName} = {getterFieldIdentifierName}Helper?.Value ?? {getterFieldIdentifierName}")); - } - - // Prepare the forwarded attributes, if any - var forwardedAttributes = - propertyInfo.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") - { - helperTypeName = $"ReactiveUI.ObservableAsPropertyHelper<{propertyType}>"; - modifiers.Add(Token(SyntaxKind.ReadOnlyKeyword)); - } - - // Construct the generated property as follows: - // - // /// - // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] - // [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - // - // public - // { - // get => ; - // } - return - ImmutableArray.Create( - FieldDeclaration(VariableDeclaration(ParseTypeName(helperTypeName))) - .AddDeclarationVariables(VariableDeclarator(getterFieldIdentifierName + "Helper")) - .AddAttributeLists( - AttributeList(SingletonSeparatedList( - Attribute(IdentifierName(AttributeDefinitions.GeneratedCode)) - .AddArgumentListArguments( - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyGenerator).FullName))), - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyGenerator).Assembly.GetName().Version.ToString())))))) - .WithOpenBracketToken(Token(TriviaList(Comment($"/// ")), SyntaxKind.OpenBracketToken, TriviaList()))) - .AddModifiers([.. modifiers]), - PropertyDeclaration(propertyType, Identifier(propertyInfo.PropertyName)) - .AddAttributeLists( - AttributeList(SingletonSeparatedList( - Attribute(IdentifierName(AttributeDefinitions.GeneratedCode)) - .AddArgumentListArguments( - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyGenerator).FullName))), - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyGenerator).Assembly.GetName().Version.ToString())))))) - .WithOpenBracketToken(Token(TriviaList(Comment($"/// ")), SyntaxKind.OpenBracketToken, TriviaList())), - AttributeList(SingletonSeparatedList(Attribute(IdentifierName(AttributeDefinitions.ExcludeFromCodeCoverage))))) - .AddAttributeLists([.. forwardedAttributes]) - .AddModifiers(Token(SyntaxKind.PublicKeyword)) - .AddAccessorListAccessors( - AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) - .WithExpressionBody(getterArrowExpression) - .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)))); - } - - internal static bool GetFieldInfoFromClass( - FieldDeclarationSyntax fieldSyntax, - IFieldSymbol fieldSymbol, - SemanticModel semanticModel, - bool? isReadonly, - CancellationToken token, - [NotNullWhen(true)] out PropertyInfo? propertyInfo, - out ImmutableArray diagnostics) - { - using var builder = ImmutableArrayBuilder.Rent(); - - // Validate the target type - if (!IsTargetTypeValid(fieldSymbol)) - { - builder.Add( - InvalidObservableAsPropertyError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); - - propertyInfo = null; - diagnostics = builder.ToImmutable(); - - return false; - } - - // Get the property type and name - var typeNameWithNullabilityAnnotations = fieldSymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); - var fieldName = fieldSymbol.Name; - var propertyName = GetGeneratedPropertyName(fieldSymbol); - var initializer = fieldSyntax.Declaration.Variables.FirstOrDefault()?.Initializer?.ToFullString(); - - // Check for name collisions - if (fieldName == propertyName) - { - builder.Add( - ReactivePropertyNameCollisionError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); - - propertyInfo = null; - diagnostics = builder.ToImmutable(); - - // If the generated property would collide, skip generating it entirely. This makes sure that - // users only get the helpful diagnostic about the collision, and not the normal compiler error - // about a definition for "Property" already existing on the target type, which might be confusing. - return false; - } - - token.ThrowIfCancellationRequested(); - - using var forwardedAttributes = ImmutableArrayBuilder.Rent(); - - // Gather attributes info - foreach (var attributeData in fieldSymbol.GetAttributes()) - { - token.ThrowIfCancellationRequested(); - - // Track the current attribute for forwarding if it is a validation attribute - if (attributeData.AttributeClass?.InheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute") == true) - { - forwardedAttributes.Add(AttributeInfo.Create(attributeData)); - } - - // 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(attributeData)); - } - - // Also track the current attribute for forwarding if it is of any of the following types: - if (attributeData.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.UIHintAttribute") == true || - attributeData.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute") == true || - attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.DisplayAttribute") == true || - attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.EditableAttribute") == true || - attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.KeyAttribute") == true || - attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.Runtime.Serialization.DataMemberAttribute") == true || - attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.Runtime.Serialization.IgnoreDataMemberAttribute") == true) - { - forwardedAttributes.Add(AttributeInfo.Create(attributeData)); - } - } - - token.ThrowIfCancellationRequested(); - - // Gather explicit forwarded attributes info - foreach (var attributeList in fieldSyntax.AttributeLists) - { - // 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 [ObservableAsProperty]. - 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 (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out var attributeTypeSymbol)) - { - builder.Add( - InvalidPropertyTargetedAttributeOnObservableAsPropertyField, - attribute, - fieldSymbol, - attribute.Name); - - continue; - } - - var attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); - - // Try to extract the forwarded attribute - if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, token, out var attributeInfo)) - { - builder.Add( - InvalidPropertyTargetedAttributeExpressionOnObservableAsPropertyField, - attribute, - fieldSymbol, - attribute.Name); - - continue; - } - - forwardedAttributes.Add(attributeInfo); - } - } - - token.ThrowIfCancellationRequested(); - - // Get the nullability info for the property - fieldSymbol.GetNullabilityInfo( - semanticModel, - out var isReferenceTypeOrUnconstraindTypeParameter, - out var includeMemberNotNullOnSetAccessor); - - token.ThrowIfCancellationRequested(); - 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( - targetInfo.FileHintName, - targetInfo.TargetName, - targetInfo.TargetNamespace, - targetInfo.TargetNamespaceWithNamespace, - targetInfo.TargetVisibility, - targetInfo.TargetType, - typeNameWithNullabilityAnnotations, - fieldName, - propertyName, - initializer, - isReferenceTypeOrUnconstraindTypeParameter, - includeMemberNotNullOnSetAccessor, - forwardedPropertyAttributes, - isReadonly == false ? string.Empty : "readonly"); - - diagnostics = builder.ToImmutable(); - - return true; - } - - /// - /// Validates the containing type for a given field being annotated. - /// - /// The input instance to process. - /// Whether or not the containing type for is valid. - private static bool IsTargetTypeValid(IFieldSymbol fieldSymbol) - { - var isObservableObject = fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.ReactiveObject"); - var isIObservableObject = fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.IReactiveObject"); - var hasObservableObjectAttribute = fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("ReactiveUI.SourceGenerators.ReactiveObjectAttribute"); - - return isIObservableObject || isObservableObject || hasObservableObjectAttribute; - } - - /// - /// Get the generated property name for an input field. - /// - /// The input instance to process. - /// The generated property name for . - private static string GetGeneratedPropertyName(IFieldSymbol fieldSymbol) - { - var propertyName = fieldSymbol.Name; - - if (propertyName.StartsWith("m_")) - { - propertyName = propertyName.Substring(2); - } - else if (propertyName.StartsWith("_")) - { - propertyName = propertyName.TrimStart('_'); - } - - return $"{char.ToUpper(propertyName[0], CultureInfo.InvariantCulture)}{propertyName.Substring(1)}"; - } - } -} diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Generator.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Generator.cs deleted file mode 100644 index bc7cfd4..0000000 --- a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Generator.cs +++ /dev/null @@ -1,87 +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.Syntax; -using ReactiveUI.SourceGenerators.Extensions; -using ReactiveUI.SourceGenerators.Helpers; -using ReactiveUI.SourceGenerators.Models; -using ReactiveUI.SourceGenerators.Reactive.Models; - -namespace ReactiveUI.SourceGenerators; - -/// -/// A source generator for generating reative properties. -/// -public sealed partial class ObservableAsPropertyGenerator -{ - private static void RunObservableAsPropertyGenerator(in IncrementalGeneratorInitializationContext context) - { - // Gather info for all annotated command methods (starting from method declarations with at least one attribute) - IncrementalValuesProvider<(HierarchyInfo Hierarchy, Result Info)> propertyInfoWithErrors = - context.SyntaxProvider - .ForAttributeWithMetadataName( - AttributeDefinitions.ObservableAsPropertyAttributeType, - static (node, _) => node is VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Parent: FieldDeclarationSyntax { Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, AttributeLists.Count: > 0 } } }, - static (context, token) => - { - var symbol = ModelExtensions.GetDeclaredSymbol(context.SemanticModel, context.TargetNode, token)!; - token.ThrowIfCancellationRequested(); - - // Skip symbols without the target attribute - if (!symbol.TryGetAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.ObservableAsPropertyAttributeType, out var attributeData)) - { - return default; - } - - // Get the can PropertyName member, if any - attributeData.TryGetNamedArgument("ReadOnly", out bool? isReadonly); - - var fieldDeclaration = (FieldDeclarationSyntax)context.TargetNode.Parent!.Parent!; - var fieldSymbol = (IFieldSymbol)context.TargetSymbol; - - // Get the hierarchy info for the target symbol, and try to gather the property info - var hierarchy = HierarchyInfo.From(fieldSymbol.ContainingType); - - token.ThrowIfCancellationRequested(); - - Execute.GetFieldInfoFromClass(fieldDeclaration, fieldSymbol, context.SemanticModel, isReadonly, token, out var propertyInfo, out var diagnostics); - - token.ThrowIfCancellationRequested(); - return (Hierarchy: hierarchy, new Result(propertyInfo, diagnostics)); - }) - .Where(static item => item.Hierarchy is not null)!; - - // Output the diagnostics - context.ReportDiagnostics(propertyInfoWithErrors.Select(static (item, _) => item.Info.Errors)); - - // Get the filtered sequence to enable caching - var propertyInfo = - propertyInfoWithErrors - .Where(static item => item.Info.Value is not null)!; - - // Split and group by containing type - var groupedPropertyInfo = - propertyInfo - .GroupBy(static item => item.Left, static item => item.Right.Value); - - // Generate the requested properties and methods - context.RegisterSourceOutput(groupedPropertyInfo, static (context, item) => - { - // Generate all member declarations for the current type - var memberDeclarations = - item.Right - .Select(Execute.GetPropertySyntax) - .SelectMany(static m => m) - .ToImmutableArray(); - - // Insert all members into the same partial type declaration - var compilationUnit = item.Key.GetCompilationUnit(memberDeclarations); - context.AddSource($"{item.Key.FilenameHint}.ObservableAsProperties.g.cs", compilationUnit); - }); - } -} diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.cs index 22cb754..5ef9461 100644 --- a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.cs +++ b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.cs @@ -18,13 +18,16 @@ namespace ReactiveUI.SourceGenerators; [Generator(LanguageNames.CSharp)] public sealed partial class ObservableAsPropertyGenerator : IIncrementalGenerator { + internal static readonly string GeneratorName = typeof(ObservableAsPropertyGenerator).FullName!; + internal static readonly string GeneratorVersion = typeof(ObservableAsPropertyGenerator).Assembly.GetName().Version.ToString(); + /// public void Initialize(IncrementalGeneratorInitializationContext context) { context.RegisterPostInitializationOutput(ctx => ctx.AddSource($"{AttributeDefinitions.ObservableAsPropertyAttributeType}.g.cs", SourceText.From(AttributeDefinitions.ObservableAsPropertyAttribute, Encoding.UTF8))); - RunObservablePropertyAsFromObservable(context); - RunObservableAsPropertyGenerator(context); + RunObservableAsPropertyFromObservable(context); + RunObservableAsPropertyFromField(context); } } diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.Execute.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.Execute.cs new file mode 100644 index 0000000..a933143 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.Execute.cs @@ -0,0 +1,267 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using ReactiveUI.SourceGenerators.Extensions; +using ReactiveUI.SourceGenerators.Helpers; +using ReactiveUI.SourceGenerators.Models; +using ReactiveUI.SourceGenerators.Reactive.Models; + +namespace ReactiveUI.SourceGenerators; + +/// +/// ReactiveGenerator. +/// +/// +public sealed partial class ObservableAsPropertyGenerator +{ + private static PropertyInfo? GetVariableInfo(in GeneratorAttributeSyntaxContext context, CancellationToken token) + { + var symbol = context.TargetSymbol; + token.ThrowIfCancellationRequested(); + + // Skip symbols without the target attribute + if (!symbol.TryGetAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.ObservableAsPropertyAttributeType, out var attributeData)) + { + return default; + } + + if (symbol is not IFieldSymbol fieldSymbol) + { + return default; + } + + // Validate the target type + if (!IsTargetTypeValid(fieldSymbol)) + { + return default; + } + + // Get the can PropertyName member, if any + attributeData.TryGetNamedArgument("ReadOnly", out bool? isReadonly); + + token.ThrowIfCancellationRequested(); + + // Get the property type and name + var typeNameWithNullabilityAnnotations = fieldSymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); + var fieldName = fieldSymbol.Name; + var propertyName = GetGeneratedPropertyName(fieldSymbol); + + var fieldDeclaration = (FieldDeclarationSyntax)context.TargetNode.Parent!.Parent!; + var initializer = fieldDeclaration.Declaration.Variables.FirstOrDefault()?.Initializer?.ToFullString(); + + // Check for name collisions + if (fieldName == propertyName) + { + return default; + } + + token.ThrowIfCancellationRequested(); + + 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 (attribute.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(); + + // Gather explicit forwarded attributes info + foreach (var attributeList in fieldDeclaration.AttributeLists) + { + // 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 [ObservableAsProperty]. + 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); + } + } + + token.ThrowIfCancellationRequested(); + + // Get the nullability info for the property + fieldSymbol.GetNullabilityInfo( + context.SemanticModel, + out var isReferenceTypeOrUnconstraindTypeParameter, + out var includeMemberNotNullOnSetAccessor); + + token.ThrowIfCancellationRequested(); + var attributes = forwardedAttributes.ToImmutable(); + var forwardedPropertyAttributes = attributes.Select(static a => a.ToString()).ToImmutableArray(); + + // Get the containing type info + var targetInfo = TargetInfo.From(fieldSymbol.ContainingType); + + return new PropertyInfo( + targetInfo.FileHintName, + targetInfo.TargetName, + targetInfo.TargetNamespace, + targetInfo.TargetNamespaceWithNamespace, + targetInfo.TargetVisibility, + targetInfo.TargetType, + typeNameWithNullabilityAnnotations, + fieldName, + propertyName, + initializer, + isReferenceTypeOrUnconstraindTypeParameter, + includeMemberNotNullOnSetAccessor, + forwardedPropertyAttributes, + isReadonly == false ? string.Empty : "readonly"); + } + + private static string GenerateSource(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, PropertyInfo[] properties) + { + var propertyDeclarations = string.Join("\n\r", properties.Select(GetPropertySyntax)); + + return $$""" +// +#pragma warning disable +#nullable enable +namespace {{containingNamespace}} +{ + /// + {{containingClassVisibility}} partial {{containingType}} {{containingTypeName}} + { + [global::System.CodeDom.Compiler.GeneratedCode("{{GeneratorName}}", "{{GeneratorVersion}}")] +{{propertyDeclarations}} + } +} +#nullable restore +#pragma warning restore +"""; + } + + private static string GetPropertySyntax(PropertyInfo propertyInfo) + { + var propertyAttributes = string.Join("\n ", AttributeDefinitions.ExcludeFromCodeCoverage.Concat(propertyInfo.ForwardedAttributes)); + + var getter = $$"""{ get => {{propertyInfo.FieldName}} = {{propertyInfo.FieldName}}Helper?.Value ?? {{propertyInfo.FieldName}}; }"""; + + // If the property is nullable, we need to add a null check to the getter + if (propertyInfo.TypeNameWithNullabilityAnnotations.EndsWith("?")) + { + getter = $$"""{ get => {{propertyInfo.FieldName}} = ({{propertyInfo.FieldName}}Helper == null ? {{propertyInfo.FieldName}} : {{propertyInfo.FieldName}}Helper.Value); }"""; + } + + var helperTypeName = $"private ReactiveUI.ObservableAsPropertyHelper<{propertyInfo.TypeNameWithNullabilityAnnotations}>?"; + + // If the property is readonly, we need to change the helper to be non-nullable + if (propertyInfo.AccessModifier == "readonly") + { + helperTypeName = $"private readonly ReactiveUI.ObservableAsPropertyHelper<{propertyInfo.TypeNameWithNullabilityAnnotations}>"; + } + + return $$""" + /// + {{helperTypeName}} {{propertyInfo.FieldName}}Helper; + + /// + {{propertyAttributes}} + public {{propertyInfo.TypeNameWithNullabilityAnnotations}} {{propertyInfo.PropertyName}} {{getter}} +"""; + } + + /// + /// Get the generated property name for an input field. + /// + /// The input instance to process. + /// The generated property name for . + private static string GetGeneratedPropertyName(IFieldSymbol fieldSymbol) + { + var propertyName = fieldSymbol.Name; + + if (propertyName.StartsWith("m_")) + { + propertyName = propertyName.Substring(2); + } + else if (propertyName.StartsWith("_")) + { + propertyName = propertyName.TrimStart('_'); + } + + return $"{char.ToUpper(propertyName[0], CultureInfo.InvariantCulture)}{propertyName.Substring(1)}"; + } + + /// + /// Validates the containing type for a given field being annotated. + /// + /// The input instance to process. + /// Whether or not the containing type for is valid. + private static bool IsTargetTypeValid(IFieldSymbol fieldSymbol) + { + var isObservableObject = fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.ReactiveObject"); + var isIObservableObject = fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.IReactiveObject"); + var hasObservableObjectAttribute = fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("ReactiveUI.SourceGenerators.ReactiveObjectAttribute"); + + return isIObservableObject || isObservableObject || hasObservableObjectAttribute; + } +} diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.cs new file mode 100644 index 0000000..b740c55 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.cs @@ -0,0 +1,59 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using ReactiveUI.SourceGenerators.Extensions; +using ReactiveUI.SourceGenerators.Helpers; + +namespace ReactiveUI.SourceGenerators; + +/// +/// A source generator for generating reative properties. +/// +public sealed partial class ObservableAsPropertyGenerator +{ + private static void RunObservableAsPropertyFromField(in IncrementalGeneratorInitializationContext context) + { + var propertyInfo = + context.SyntaxProvider + .ForAttributeWithMetadataName( + AttributeDefinitions.ObservableAsPropertyAttributeType, + static (node, _) => node is VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Parent: FieldDeclarationSyntax { Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, AttributeLists.Count: > 0 } } }, + static (context, token) => GetVariableInfo(context, token)) + .Where(x => x != null) + .Select((x, _) => x!) + .Collect(); + + // Generate the requested properties and methods + context.RegisterSourceOutput(propertyInfo, static (context, input) => + { + var groupedPropertyInfo = input.GroupBy( + static info => (info.FileHintName, info.TargetName, info.TargetNamespace, info.TargetVisibility, info.TargetType), + static info => info) + .ToImmutableArray(); + + if (groupedPropertyInfo.Length == 0) + { + return; + } + + foreach (var grouping in groupedPropertyInfo) + { + var items = grouping.ToImmutableArray(); + + if (items.Length == 0) + { + continue; + } + + var source = GenerateSource(grouping.Key.TargetName, grouping.Key.TargetNamespace, grouping.Key.TargetVisibility, grouping.Key.TargetType, [.. grouping]); + context.AddSource($"{grouping.Key.FileHintName}.ObservableAsProperties.g.cs", source); + } + }); + } +} diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs similarity index 98% rename from src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.Execute.cs rename to src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs index 0dbbfb8..b861a96 100644 --- a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs @@ -78,7 +78,7 @@ internal static ImmutableArray GetPropertySyntax(Observ AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyGenerator).FullName))), AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyGenerator).Assembly.GetName().Version.ToString())))))) .WithOpenBracketToken(Token(TriviaList(Comment($"/// ")), SyntaxKind.OpenBracketToken, TriviaList())), - AttributeList(SingletonSeparatedList(Attribute(IdentifierName(AttributeDefinitions.ExcludeFromCodeCoverage))))) + AttributeList(SingletonSeparatedList(Attribute(IdentifierName(AttributeDefinitions.ExcludeFromCodeCoverageString))))) .AddAttributeLists([.. forwardedAttributes]) .AddModifiers(Token(SyntaxKind.PublicKeyword)) .AddAccessorListAccessors( @@ -113,7 +113,7 @@ internal static MethodDeclarationSyntax GetPropertyInitiliser(ObservableMethodIn .AddArgumentListArguments( AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyGenerator).FullName))), AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyGenerator).Assembly.GetName().Version.ToString())))))), - AttributeList(SingletonSeparatedList(Attribute(IdentifierName(AttributeDefinitions.ExcludeFromCodeCoverage))))) + AttributeList(SingletonSeparatedList(Attribute(IdentifierName(AttributeDefinitions.ExcludeFromCodeCoverageString))))) .WithModifiers(TokenList(Token(SyntaxKind.ProtectedKeyword))) .WithBody(Block(propertyInitilisers.ToImmutable())); } diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.cs similarity index 99% rename from src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.cs rename to src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.cs index 4e6845b..1caaa92 100644 --- a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.cs +++ b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.cs @@ -24,7 +24,7 @@ namespace ReactiveUI.SourceGenerators; /// public sealed partial class ObservableAsPropertyGenerator { - private static void RunObservablePropertyAsFromObservable(in IncrementalGeneratorInitializationContext context) + private static void RunObservableAsPropertyFromObservable(in IncrementalGeneratorInitializationContext context) { // Gather info for all annotated command methods (starting from method declarations with at least one attribute) IncrementalValuesProvider<(HierarchyInfo Hierarchy, Result Info)> propertyInfoWithErrors = diff --git a/src/ReactiveUI.SourceGenerators/Reactive/ReactiveGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators/Reactive/ReactiveGenerator.Execute.cs index 988dc1c..837af44 100644 --- a/src/ReactiveUI.SourceGenerators/Reactive/ReactiveGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators/Reactive/ReactiveGenerator.Execute.cs @@ -26,8 +26,6 @@ public sealed partial class ReactiveGenerator internal static readonly string GeneratorName = typeof(ReactiveGenerator).FullName!; internal static readonly string GeneratorVersion = typeof(ReactiveGenerator).Assembly.GetName().Version.ToString(); - private static readonly string[] excludeFromCodeCoverage = ["[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]"]; - /// /// Gets the observable method information. /// @@ -245,7 +243,7 @@ private static string GetPropertySyntax(PropertyInfo propertyInfo) setModifier = string.Empty; } - var propertyAttributes = string.Join("\n ", excludeFromCodeCoverage.Concat(propertyInfo.ForwardedAttributes)); + var propertyAttributes = string.Join("\n ", AttributeDefinitions.ExcludeFromCodeCoverage.Concat(propertyInfo.ForwardedAttributes)); if (propertyInfo.IncludeMemberNotNullOnSetAccessor) { diff --git a/src/ReactiveUI.SourceGenerators/ReactiveCommand/ReactiveCommandGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators/ReactiveCommand/ReactiveCommandGenerator.Execute.cs index 43a0b77..5be0916 100644 --- a/src/ReactiveUI.SourceGenerators/ReactiveCommand/ReactiveCommandGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators/ReactiveCommand/ReactiveCommandGenerator.Execute.cs @@ -34,7 +34,6 @@ public partial class ReactiveCommandGenerator private const string CreateO = ".CreateFromObservable"; private const string CreateT = ".CreateFromTask"; private const string CanExecute = "CanExecute"; - private static readonly string[] excludeFromCodeCoverage = ["[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]"]; private static CommandInfo? GetMethodInfo(in GeneratorAttributeSyntaxContext context, CancellationToken token) { @@ -164,7 +163,7 @@ private static string GetCommandSyntax(CommandInfo commandExtensionInfo) } // Prepare any forwarded property attributes - var forwardedPropertyAttributesString = string.Join("\n ", excludeFromCodeCoverage.Concat(commandExtensionInfo.ForwardedPropertyAttributes)); + var forwardedPropertyAttributesString = string.Join("\n ", AttributeDefinitions.ExcludeFromCodeCoverage.Concat(commandExtensionInfo.ForwardedPropertyAttributes)); return $$""" diff --git a/src/ReactiveUI.SourceGenerators/RoutedControlHost/RoutedControlHostGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators/RoutedControlHost/RoutedControlHostGenerator.Execute.cs index e808378..ecb2af1 100644 --- a/src/ReactiveUI.SourceGenerators/RoutedControlHost/RoutedControlHostGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators/RoutedControlHost/RoutedControlHostGenerator.Execute.cs @@ -26,8 +26,6 @@ public partial class RoutedControlHostGenerator 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) { if (!(context.TargetNode is ClassDeclarationSyntax declaredClass && declaredClass.Modifiers.Any(SyntaxKind.PartialKeyword))) @@ -86,7 +84,7 @@ public partial class RoutedControlHostGenerator 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)); + var forwardedAttributesString = string.Join("\n ", AttributeDefinitions.ExcludeFromCodeCoverage.Concat(vmcInfo.ForwardedAttributes)); return $$""" @@ -108,7 +106,7 @@ namespace {{containingNamespace}} {{forwardedAttributesString}} [DefaultProperty("ViewModel")] [global::System.CodeDom.Compiler.GeneratedCode("{{GeneratorName}}", "{{GeneratorVersion}}")] - {{containingClassVisibility}} partial class {{containingTypeName}} : {{vmcInfo.BaseTypeName}}, IReactiveObject + {{containingClassVisibility}} partial {{containingType}} {{containingTypeName}} : {{vmcInfo.BaseTypeName}}, IReactiveObject { private readonly CompositeDisposable _disposables = []; private RoutingState? _router; diff --git a/src/ReactiveUI.SourceGenerators/ViewModelControlHost/ViewModelControlHostGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators/ViewModelControlHost/ViewModelControlHostGenerator.Execute.cs index a362f9a..1c3b2ae 100644 --- a/src/ReactiveUI.SourceGenerators/ViewModelControlHost/ViewModelControlHostGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators/ViewModelControlHost/ViewModelControlHostGenerator.Execute.cs @@ -26,8 +26,6 @@ public partial class ViewModelControlHostGenerator 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) { if (!(context.TargetNode is ClassDeclarationSyntax declaredClass && declaredClass.Modifiers.Any(SyntaxKind.PartialKeyword))) @@ -79,7 +77,7 @@ public partial class ViewModelControlHostGenerator 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)); + var forwardedAttributesString = string.Join("\n ", AttributeDefinitions.ExcludeFromCodeCoverage.Concat(vmcInfo.ForwardedAttributes)); return $$""" @@ -101,7 +99,7 @@ namespace {{containingNamespace}} {{forwardedAttributesString}} [DefaultProperty("ViewModel")] [global::System.CodeDom.Compiler.GeneratedCode("{{GeneratorName}}", "{{GeneratorVersion}}")] - public partial class {{containingTypeName}} : {{vmcInfo.ViewModelTypeName}}, IReactiveObject, IViewFor + {{containingClassVisibility}} partial {{containingType}} {{containingTypeName}} : {{vmcInfo.ViewModelTypeName}}, IReactiveObject, IViewFor { private readonly CompositeDisposable _disposables = []; private Control? _defaultContent;