diff --git a/README.md b/README.md index 5c286bc..3688496 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ ReactiveUI Source Generators automatically generate ReactiveUI objects to stream - `[IViewFor(nameof(ViewModelName))]` - `[RoutedControlHost("YourNameSpace.CustomControl")]` - `[ViewModelControlHost("YourNameSpace.CustomControl")]` +- `[BindableDerivedList]` Generates a derived list from a ReadOnlyObservableCollection backing field ### Compatibility Notes - For ReactiveUI versions **older than V19.5.31**, all `[ReactiveCommand]` options are supported except for async methods with a `CancellationToken`. @@ -521,6 +522,18 @@ public partial class MyReactiveControl : UserControl } ``` +### Usage ReadOnlyObservableCollection + +```csharp +using ReactiveUI.SourceGenerators; + +public partial class MyReactiveClass +{ + [BindableDerivedList] + private readonly ReadOnlyObservableCollection _myList; +} +``` + ## Platform specific Attributes ### WinForms diff --git a/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/BindableDerivedListGeneratorTests.FromReactiveProperties#ReactiveUI.SourceGenerators.BindableDerivedListAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/BindableDerivedListGeneratorTests.FromReactiveProperties#ReactiveUI.SourceGenerators.BindableDerivedListAttribute.g.verified.cs new file mode 100644 index 0000000..bc8d779 --- /dev/null +++ b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/BindableDerivedListGeneratorTests.FromReactiveProperties#ReactiveUI.SourceGenerators.BindableDerivedListAttribute.g.verified.cs @@ -0,0 +1,21 @@ +//HintName: ReactiveUI.SourceGenerators.BindableDerivedListAttribute.g.cs +// Copyright (c) 2025 .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; + +// +#pragma warning disable +#nullable enable +namespace ReactiveUI.SourceGenerators; + +/// +/// ReactiveAttribute. +/// +/// +[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] +internal sealed class BindableDerivedListAttribute : Attribute; +#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 1d7a0a5..02ad820 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/ReactiveUI.SourceGenerators.Tests.csproj +++ b/src/ReactiveUI.SourceGenerator.Tests/ReactiveUI.SourceGenerators.Tests.csproj @@ -35,6 +35,7 @@ + diff --git a/src/ReactiveUI.SourceGenerator.Tests/TestHelper.cs b/src/ReactiveUI.SourceGenerator.Tests/TestHelper.cs index c004667..bb77a24 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/TestHelper.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/TestHelper.cs @@ -33,10 +33,10 @@ namespace ReactiveUI.SourceGenerator.Tests; public sealed class TestHelper(ITestOutputHelper testOutput) : IDisposable where T : IIncrementalGenerator, new() { +#pragma warning disable CS0618 // Type or member is obsolete /// /// Represents the NuGet library dependency for the Splat library. /// -#pragma warning disable CS0618 // Type or member is obsolete private static readonly LibraryRange SplatLibrary = new("Splat", VersionRange.AllStableFloating, LibraryDependencyTarget.Package); @@ -90,6 +90,7 @@ public string VerifiedFilePath() nameof(IViewForGenerator) => "IVIEWFOR", nameof(RoutedControlHostGenerator) => "ROUTEDHOST", nameof(ViewModelControlHostGenerator) => "CONTROLHOST", + nameof(BindableDerivedListGenerator) => "DERIVEDLIST", _ => name, }; } diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/BindableDerivedListGeneratorTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/BindableDerivedListGeneratorTests.cs new file mode 100644 index 0000000..ae1f173 --- /dev/null +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/BindableDerivedListGeneratorTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using ReactiveUI.SourceGenerators; +using Xunit.Abstractions; + +namespace ReactiveUI.SourceGenerator.Tests; + +/// +/// BindableDerivedListGeneratorTests. +/// +public class BindableDerivedListGeneratorTests(ITestOutputHelper output) : TestBase(output) +{ + /// + /// Tests that the source generator correctly generates reactive properties. + /// + /// A task to monitor the async. + [Fact] + public Task FromReactiveProperties() + { + // Arrange: Setup the source code that matches the generator input expectations. + const string sourceCode = """ + using System.Collections.ObjectModel; + using DynamicData; + + namespace TestNs; + + public partial class TestVM + { + [BindableDerivedList] + private ReadOnlyObservableCollection _test1; + } + """; + + // Act: Initialize the helper and run the generator. Assert: Verify the generated code. + return TestHelper.TestPass(sourceCode); + } +} diff --git a/src/ReactiveUI.SourceGenerators.Execute/Person.cs b/src/ReactiveUI.SourceGenerators.Execute/Person.cs new file mode 100644 index 0000000..0e2ceee --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.Execute/Person.cs @@ -0,0 +1,25 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using ReactiveUI; +using ReactiveUI.SourceGenerators; + +namespace SGReactiveUI.SourceGenerators.Test; + +/// +/// Person. +/// +/// +public partial class Person : ReactiveObject +{ + /// + /// Gets or sets a value indicating whether this is deleted. + /// + /// + /// true if deleted; otherwise, false. + /// + [Reactive] + public bool Deleted { get; set; } +} diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs index 49f245b..a6d9504 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs @@ -3,6 +3,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Reactive; using System.Reactive.Concurrency; @@ -11,6 +12,7 @@ using System.Reactive.Subjects; using System.Runtime.Serialization; using System.Text.Json.Serialization; +using DynamicData; using ReactiveUI; using ReactiveUI.SourceGenerators; @@ -66,6 +68,12 @@ public partial class TestViewModel : ReactiveObject, IActivatableViewModel, IDis [Reactive(SetModifier = AccessModifier.Init, UseRequired = true)] private string _mustBeSet; + [Reactive] + private IEnumerable _people = [new Person()]; + + [BindableDerivedList] + private ReadOnlyObservableCollection? _visiblePeople; + /// /// Initializes a new instance of the class. /// @@ -186,6 +194,15 @@ public TestViewModel() _observableAsPropertyFromPropertyHelper = _fromPartialTestSubject.ToProperty(this, x => x.ObservableAsPropertyFromProperty); _fromPartialTestSubject.OnNext(11); Console.Out.WriteLine($"Observable updated, value should be 11, value is : {ObservableAsPropertyFromProperty}"); + + this.WhenAnyValue(vm => vm.People) + .Subscribe(people => people + .AsObservableChangeSet() + .AutoRefresh(x => x.Deleted) + .Filter(x => !x.Deleted) + .Bind(out _visiblePeople) + .Subscribe()); + Console.ReadLine(); } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/AnalyzerReleases.Shipped.md b/src/ReactiveUI.SourceGenerators.Roslyn/AnalyzerReleases.Shipped.md index c7fce7a..3566637 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/AnalyzerReleases.Shipped.md +++ b/src/ReactiveUI.SourceGenerators.Roslyn/AnalyzerReleases.Shipped.md @@ -22,7 +22,7 @@ RXUISG0015 | ReactiveUI.SourceGenerators.ReactiveGenerator | Error | See https:/ RXUISG0017 | ReactiveUI.SourceGenerators.ObservableAsPropertyFromObservableGenerator | Error | See https://www.reactiveui.net/docs/handbook/view-models/boilerplate-code.html RXUISG0018 | ReactiveUI.SourceGenerators.ObservableAsPropertyFromObservableGenerator | Error | See https://www.reactiveui.net/docs/handbook/view-models/boilerplate-code.html RXUISG0018 | ReactiveUI.SourceGenerators.ReactiveGenerator | Error | See https://www.reactiveui.net/docs/handbook/view-models/boilerplate-code.html - +RXUISG0019 | ReactiveUI.SourceGenerators.BindableDerivedListGenerator | Error | See https://www.reactiveui.net/docs/handbook/view-models/boilerplate-code.html ## Rules Shipped in ReactiveUI.SourceGenerators @@ -83,3 +83,6 @@ This rule checks if the `ObservableAsProperty` has Invalid class inheritance, mu - RXUISG0018 - ReactiveGenerator This rule checks if the `Reactive` has Invalid class inheritance, must inherit from `ReactiveObject`. + +- RXUISG0019 - BindableDerivedListGenerator +- This rule checks if the `BindableDerivedList` has Invalid property inheritance type. diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/AttributeDefinitions.cs b/src/ReactiveUI.SourceGenerators.Roslyn/AttributeDefinitions.cs index b678a65..a4802f5 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/AttributeDefinitions.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/AttributeDefinitions.cs @@ -435,33 +435,29 @@ internal sealed class RoutedControlHostAttribute(string? baseType) : Attribute; #pragma warning restore """; - public const string IsExternalInitType = "System.Runtime.CompilerServices.IsExternalInit"; + public const string BindableDerivedListAttributeType = "ReactiveUI.SourceGenerators.BindableDerivedListAttribute"; - public static string IsExternalInit => $$""" + public static string BindableDerivedListAttribute => $$""" // 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. - -// -#pragma warning disable -#if !NET5_0_OR_GREATER - -namespace System.Runtime.CompilerServices; +using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; +// +#pragma warning disable +#nullable enable +namespace ReactiveUI.SourceGenerators; /// -/// Reserved to be used by the compiler for tracking metadata. This class should not be used by developers in source code. +/// ReactiveAttribute. /// -[ExcludeFromCodeCoverage] -[DebuggerNonUserCode] -static class IsExternalInit; - -#endif - +/// +[global::System.CodeDom.Compiler.GeneratedCode("{{ReactiveGenerator.GeneratorName}}", "{{ReactiveGenerator.GeneratorVersion}}")] +[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] +internal sealed class BindableDerivedListAttribute : Attribute; +#nullable restore #pragma warning restore """; } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/BindableDerivedListGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/BindableDerivedListGenerator.Execute.cs new file mode 100644 index 0000000..9d1b9e0 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/BindableDerivedListGenerator.Execute.cs @@ -0,0 +1,185 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using ReactiveUI.SourceGenerators.BindableDerivedList.Models; +using ReactiveUI.SourceGenerators.Extensions; +using ReactiveUI.SourceGenerators.Helpers; +using ReactiveUI.SourceGenerators.Models; +using static ReactiveUI.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace ReactiveUI.SourceGenerators; + +/// +/// BindableDerivedListGenerator. +/// +public sealed partial class BindableDerivedListGenerator +{ + internal static readonly string GeneratorName = typeof(BindableDerivedListGenerator).FullName!; + internal static readonly string GeneratorVersion = typeof(BindableDerivedListGenerator).Assembly.GetName().Version.ToString(); + + private static Result? GetVariableInfo(in GeneratorAttributeSyntaxContext context, CancellationToken token) + { + using var builder = ImmutableArrayBuilder.Rent(); + var symbol = context.TargetSymbol; + + if (!symbol.TryGetAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.BindableDerivedListAttributeType, out var attributeData)) + { + return default; + } + + if (symbol is not IFieldSymbol fieldSymbol) + { + return default; + } + + token.ThrowIfCancellationRequested(); + + // Get the property type and name + var typeNameWithNullabilityAnnotations = fieldSymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); + + // Check that the type is a ReadOnlyObservableCollection + if (!fieldSymbol.Type.HasOrInheritsFromFullyQualifiedMetadataNameStartingWith("System.Collections.ObjectModel.ReadOnlyObservableCollection")) + { + builder.Add( + ReadOnlyObservableCollectionTypeRequiredError, + fieldSymbol, + fieldSymbol.ContainingType, + fieldSymbol.Name); + return new(default, builder.ToImmutable()); + } + + token.ThrowIfCancellationRequested(); + + var fieldName = fieldSymbol.Name; + var propertyName = fieldSymbol.GetGeneratedPropertyName(); + + if (fieldName == propertyName) + { + builder.Add( + ReactivePropertyNameCollisionError, + fieldSymbol, + fieldSymbol.ContainingType, + fieldSymbol.Name); + return new(default, builder.ToImmutable()); + } + + token.ThrowIfCancellationRequested(); + + // Get the nullability info for the property + fieldSymbol.GetNullabilityInfo( + context.SemanticModel, + out var isReferenceTypeOrUnconstraindTypeParameter, + out var includeMemberNotNullOnSetAccessor); + + token.ThrowIfCancellationRequested(); + var fieldDeclaration = (FieldDeclarationSyntax)context.TargetNode.Parent!.Parent!; + + context.GetForwardedAttributes( + builder, + fieldSymbol, + fieldDeclaration.AttributeLists, + token, + out var forwardedAttributesString); + + token.ThrowIfCancellationRequested(); + + // Get the containing type info + var targetInfo = TargetInfo.From(fieldSymbol.ContainingType); + + token.ThrowIfCancellationRequested(); + + return new( + new( + targetInfo, + typeNameWithNullabilityAnnotations, + fieldName, + propertyName, + isReferenceTypeOrUnconstraindTypeParameter, + includeMemberNotNullOnSetAccessor, + forwardedAttributesString), + builder.ToImmutable()); + } + + private static string GenerateSource(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, BindableDerivedListInfo[] properties) + { + // Get Parent class details from properties.ParentInfo + var (parentClassDeclarationsString, closingBrackets) = TargetInfo.GenerateParentClassDeclarations(properties.Select(p => p.TargetInfo.ParentInfo).ToArray()); + + var classes = GenerateClassWithProperties(containingTypeName, containingNamespace, containingClassVisibility, containingType, properties); + + return +$$""" +// +using System.Collections.ObjectModel; +using DynamicData; +using ReactiveUI; + +#pragma warning disable +#nullable enable + +namespace {{containingNamespace}} +{ + {{parentClassDeclarationsString}}{{classes}}{{closingBrackets}} +} +#nullable restore +#pragma warning restore +"""; + } + + /// + /// Generates the source code. + /// + /// The contain type name. + /// The containing namespace. + /// The containing class visibility. + /// The containing type. + /// The properties. + /// The value. + private static string GenerateClassWithProperties(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, BindableDerivedListInfo[] properties) + { + // Includes 2 tabs from the property declarations so no need to add them here. + var propertyDeclarations = string.Join("\n", properties.Select(GetPropertySyntax)); + + return +$$""" +/// + /// Partial class for the {{containingTypeName}} which contains ReactiveUI Reactive property initialization. + /// + {{containingClassVisibility}} partial {{containingType}} {{containingTypeName}} + { + [global::System.CodeDom.Compiler.GeneratedCode("{{GeneratorName}}", "{{GeneratorVersion}}")] +{{propertyDeclarations}} + } +"""; + } + + /// + /// Generates property declarations for the given observable method information. + /// + /// Metadata about the observable property. + /// A string containing the generated code for the property. + private static string GetPropertySyntax(BindableDerivedListInfo propertyInfo) + { + if (propertyInfo.PropertyName is null) + { + return string.Empty; + } + + var propertyAttributes = string.Join("\n ", AttributeDefinitions.ExcludeFromCodeCoverage.Concat(propertyInfo.ForwardedAttributes)); + + return +$$""" + /// + {{propertyAttributes}} + {{propertyInfo.TargetInfo.TargetVisibility}} {{propertyInfo.TypeNameWithNullabilityAnnotations}} {{propertyInfo.PropertyName}} => {{propertyInfo.FieldName}}; +"""; + } +} diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/BindableDerivedListGenerator.cs b/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/BindableDerivedListGenerator.cs new file mode 100644 index 0000000..bef55e7 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/BindableDerivedListGenerator.cs @@ -0,0 +1,80 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using ReactiveUI.SourceGenerators.Helpers; + +namespace ReactiveUI.SourceGenerators; + +/// +/// A source generator for generating BindableDerivedList properties. +/// +[Generator(LanguageNames.CSharp)] +public sealed partial class BindableDerivedListGenerator : IIncrementalGenerator +{ + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(ctx => + { + // Add the BindableDerivedListAttribute to the compilation + ctx.AddSource($"{AttributeDefinitions.BindableDerivedListAttributeType}.g.cs", SourceText.From(AttributeDefinitions.BindableDerivedListAttribute, Encoding.UTF8)); + }); + + // Gather info for all annotated variable with at least one attribute. + var bindableDerivedListInfo = + context.SyntaxProvider + .ForAttributeWithMetadataName( + AttributeDefinitions.BindableDerivedListAttributeType, + 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 + context.RegisterSourceOutput(bindableDerivedListInfo, static (context, input) => + { + foreach (var diagnostic in input.SelectMany(static x => x.Errors)) + { + // Output the diagnostics + context.ReportDiagnostic(diagnostic.ToDiagnostic()); + } + + // Gather all the properties that are valid and group them by the target information. + var groupedPropertyInfo = input + .Where(static x => x.Value != null) + .Select(static x => x.Value!).GroupBy( + static info => (info.TargetInfo.FileHintName, info.TargetInfo.TargetName, info.TargetInfo.TargetNamespace, info.TargetInfo.TargetVisibility, info.TargetInfo.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}.BindableDerivedList.g.cs", source); + } + }); + } +} diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/Models/BindableDerivedListInfo.cs b/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/Models/BindableDerivedListInfo.cs new file mode 100644 index 0000000..d9c5b22 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/Models/BindableDerivedListInfo.cs @@ -0,0 +1,21 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using ReactiveUI.SourceGenerators.Helpers; +using ReactiveUI.SourceGenerators.Models; + +namespace ReactiveUI.SourceGenerators.BindableDerivedList.Models; + +/// +/// A model with gathered info on a given field. +/// +internal sealed record BindableDerivedListInfo( + TargetInfo TargetInfo, + string TypeNameWithNullabilityAnnotations, + string FieldName, + string PropertyName, + bool IsReferenceTypeOrUnconstrainedTypeParameter, + bool IncludeMemberNotNullOnSetAccessor, + EquatableArray ForwardedAttributes); diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/ITypeSymbolExtensions.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/ITypeSymbolExtensions.cs index 585320b..e37b1d1 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/ITypeSymbolExtensions.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/ITypeSymbolExtensions.cs @@ -56,6 +56,25 @@ public static bool InheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeS return false; } + /// + /// Checks whether or not a given has or inherits from a specified type. + /// + /// The target instance to check. + /// The full name of the type to check for inheritance. + /// Whether or not is or inherits from . + public static bool HasOrInheritsFromFullyQualifiedMetadataNameStartingWith(this ITypeSymbol typeSymbol, string name) + { + for (var currentType = typeSymbol; currentType is not null; currentType = currentType.BaseType) + { + if (currentType.ContainsFullyQualifiedMetadataName(name)) + { + return true; + } + } + + return false; + } + /// /// Checks whether or not a given inherits from a specified type. /// diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/DiagnosticDescriptors.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/DiagnosticDescriptors.cs index 04ddbab..208178a 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/DiagnosticDescriptors.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/DiagnosticDescriptors.cs @@ -151,4 +151,17 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "The fields annotated with [Reactive] or [ObservableAsProperty] must be part of a class that inherits from ReactiveObject.", helpLinkUri: "https://www.reactiveui.net/docs/handbook/view-models/boilerplate-code.html"); + + /// + /// The invalid reactive object error. + /// + public static readonly DiagnosticDescriptor ReadOnlyObservableCollectionTypeRequiredError = new DiagnosticDescriptor( + id: "RXUISG0019", + title: "Invalid field, does not inherit ReadOnlyObservableCollection", + messageFormat: "The field {0}.{1} cannot be used to generate an ReadOnlyObservableCollection", + category: typeof(BindableDerivedListGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "The fields annotated with [BindableDerivedList] must inherit from ReadOnlyObservableCollection.", + helpLinkUri: "https://www.reactiveui.net/docs/handbook/view-models/boilerplate-code.html"); } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveUI.SourceGenerators.Roslyn.projitems b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveUI.SourceGenerators.Roslyn.projitems index 538ce7c..715e996 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveUI.SourceGenerators.Roslyn.projitems +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveUI.SourceGenerators.Roslyn.projitems @@ -10,6 +10,8 @@ + + @@ -75,5 +77,6 @@ + \ No newline at end of file