diff --git a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Extensions/FieldSyntaxExtensions.cs b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Extensions/FieldSyntaxExtensions.cs
new file mode 100644
index 0000000..d8e3f10
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Extensions/FieldSyntaxExtensions.cs
@@ -0,0 +1,39 @@
+// 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;
+
+namespace ReactiveUI.SourceGenerators.Extensions;
+
+internal static class FieldSyntaxExtensions
+{
+ ///
+ /// Validates the containing type for a given field being annotated.
+ ///
+ /// The input instance to process.
+ /// Whether or not the containing type for is valid.
+ internal static bool IsTargetTypeValid(this 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;
+ }
+
+ ///
+ /// Validates the containing type for a given field being annotated.
+ ///
+ /// The input instance to process.
+ /// Whether or not the containing type for is valid.
+ internal static bool IsTargetTypeValid(this IPropertySymbol propertySymbol)
+ {
+ var isObservableObject = propertySymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.ReactiveObject");
+ var isIObservableObject = propertySymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.IReactiveObject");
+ var hasObservableObjectAttribute = propertySymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("ReactiveUI.SourceGenerators.ReactiveObjectAttribute");
+
+ return isIObservableObject || isObservableObject || hasObservableObjectAttribute;
+ }
+}
diff --git a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Extensions/ISymbolExtensions.cs b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Extensions/ISymbolExtensions.cs
new file mode 100644
index 0000000..5883393
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Extensions/ISymbolExtensions.cs
@@ -0,0 +1,33 @@
+// 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;
+
+namespace ReactiveUI.SourceGenerators.Extensions;
+
+///
+/// Extension methods for the type.
+///
+internal static class ISymbolExtensions
+{
+ ///
+ /// Checks whether or not a given symbol has an attribute with the specified fully qualified metadata name.
+ ///
+ /// The input instance to check.
+ /// The attribute name to look for.
+ /// Whether or not has an attribute with the specified name.
+ public static bool HasAttributeWithFullyQualifiedMetadataName(this ISymbol symbol, string name)
+ {
+ foreach (var attribute in symbol.GetAttributes())
+ {
+ if (attribute.AttributeClass?.HasFullyQualifiedMetadataName(name) == true)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Extensions/ITypeSymbolExtensions.cs b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Extensions/ITypeSymbolExtensions.cs
new file mode 100644
index 0000000..df97e3e
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Extensions/ITypeSymbolExtensions.cs
@@ -0,0 +1,122 @@
+// 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 Microsoft.CodeAnalysis;
+using ReactiveUI.SourceGenerators.Helpers;
+
+namespace ReactiveUI.SourceGenerators.Extensions;
+
+///
+/// Extension methods for the type.
+///
+internal static class ITypeSymbolExtensions
+{
+ ///
+ /// Checks whether or not a given inherits from a specified type.
+ ///
+ /// The target instance to check.
+ /// The full name of the type to check for inheritance.
+ /// Whether or not inherits from .
+ public static bool InheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeSymbol, string name)
+ {
+ var baseType = typeSymbol.BaseType;
+
+ while (baseType is not null)
+ {
+ if (baseType.HasFullyQualifiedMetadataName(name))
+ {
+ return true;
+ }
+
+ baseType = baseType.BaseType;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Checks whether or not a given has or inherits a specified attribute.
+ ///
+ /// The target instance to check.
+ /// The name of the attribute to look for.
+ /// Whether or not has an attribute with the specified type name.
+ public static bool HasOrInheritsAttributeWithFullyQualifiedMetadataName(this ITypeSymbol typeSymbol, string name)
+ {
+ for (var currentType = typeSymbol; currentType is not null; currentType = currentType.BaseType)
+ {
+ if (currentType.HasAttributeWithFullyQualifiedMetadataName(name))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Checks whether or not a given type symbol has a specified fully qualified metadata name.
+ ///
+ /// The input instance to check.
+ /// The full name to check.
+ /// Whether has a full name equals to .
+ public static bool HasFullyQualifiedMetadataName(this ITypeSymbol symbol, string name)
+ {
+ using var builder = ImmutableArrayBuilder.Rent();
+
+ symbol.AppendFullyQualifiedMetadataName(builder);
+
+ return builder.WrittenSpan.StartsWith(name.AsSpan());
+ }
+
+ ///
+ /// Appends the fully qualified metadata name for a given symbol to a target builder.
+ ///
+ /// The input instance.
+ /// The target instance.
+ private static void AppendFullyQualifiedMetadataName(this ITypeSymbol symbol, ImmutableArrayBuilder builder)
+ {
+ static void BuildFrom(ISymbol? symbol, ImmutableArrayBuilder builder)
+ {
+ switch (symbol)
+ {
+ // Namespaces that are nested also append a leading '.'
+ case INamespaceSymbol { ContainingNamespace.IsGlobalNamespace: false }:
+ BuildFrom(symbol.ContainingNamespace, builder);
+ builder.Add('.');
+ builder.AddRange(symbol.MetadataName.AsSpan());
+ break;
+
+ // Other namespaces (ie. the one right before global) skip the leading '.'
+ case INamespaceSymbol { IsGlobalNamespace: false }:
+ builder.AddRange(symbol.MetadataName.AsSpan());
+ break;
+
+ // Types with no namespace just have their metadata name directly written
+ case ITypeSymbol { ContainingSymbol: INamespaceSymbol { IsGlobalNamespace: true } }:
+ builder.AddRange(symbol.MetadataName.AsSpan());
+ break;
+
+ // Types with a containing non-global namespace also append a leading '.'
+ case ITypeSymbol { ContainingSymbol: INamespaceSymbol namespaceSymbol }:
+ BuildFrom(namespaceSymbol, builder);
+ builder.Add('.');
+ builder.AddRange(symbol.MetadataName.AsSpan());
+ break;
+
+ // Nested types append a leading '+'
+ case ITypeSymbol { ContainingSymbol: ITypeSymbol typeSymbol }:
+ BuildFrom(typeSymbol, builder);
+ builder.Add('+');
+ builder.AddRange(symbol.MetadataName.AsSpan());
+ break;
+ default:
+ break;
+ }
+ }
+
+ BuildFrom(symbol, builder);
+ }
+}
diff --git a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Generated/PolySharp.SourceGenerators/PolySharp.SourceGenerators.PolyfillsGenerator/System.Diagnostics.CodeAnalysis.NotNullWhenAttribute.g.cs b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Generated/PolySharp.SourceGenerators/PolySharp.SourceGenerators.PolyfillsGenerator/System.Diagnostics.CodeAnalysis.NotNullWhenAttribute.g.cs
new file mode 100644
index 0000000..7c47fad
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Generated/PolySharp.SourceGenerators/PolySharp.SourceGenerators.PolyfillsGenerator/System.Diagnostics.CodeAnalysis.NotNullWhenAttribute.g.cs
@@ -0,0 +1,29 @@
+//
+#pragma warning disable
+#nullable enable annotations
+
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Diagnostics.CodeAnalysis
+{
+ ///
+ /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it.
+ ///
+ [global::System.AttributeUsage(global::System.AttributeTargets.Parameter, Inherited = false)]
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ internal sealed class NotNullWhenAttribute : global::System.Attribute
+ {
+ ///
+ /// Initializes the attribute with the specified return value condition.
+ ///
+ /// The return value condition. If the method returns this value, the associated parameter will not be null.
+ public NotNullWhenAttribute(bool returnValue)
+ {
+ ReturnValue = returnValue;
+ }
+
+ /// Gets the return value condition.
+ public bool ReturnValue { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Generated/PolySharp.SourceGenerators/PolySharp.SourceGenerators.PolyfillsGenerator/System.Diagnostics.CodeAnalysis.UnscopedRefAttribute.g.cs b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Generated/PolySharp.SourceGenerators/PolySharp.SourceGenerators.PolyfillsGenerator/System.Diagnostics.CodeAnalysis.UnscopedRefAttribute.g.cs
new file mode 100644
index 0000000..9757fde
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Generated/PolySharp.SourceGenerators/PolySharp.SourceGenerators.PolyfillsGenerator/System.Diagnostics.CodeAnalysis.UnscopedRefAttribute.g.cs
@@ -0,0 +1,44 @@
+//
+#pragma warning disable
+#nullable enable annotations
+
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Diagnostics.CodeAnalysis
+{
+ ///
+ /// Used to indicate a byref escapes and is not scoped.
+ ///
+ ///
+ ///
+ /// There are several cases where the C# compiler treats a as implicitly
+ /// - where the compiler does not allow the to escape the method.
+ ///
+ ///
+ /// For example:
+ ///
+ /// - for instance methods.
+ /// - parameters that refer to types.
+ /// - parameters.
+ ///
+ ///
+ ///
+ /// This attribute is used in those instances where the should be allowed to escape.
+ ///
+ ///
+ /// Applying this attribute, in any form, has impact on consumers of the applicable API. It is necessary for
+ /// API authors to understand the lifetime implications of applying this attribute and how it may impact their users.
+ ///
+ ///
+ [global::System.AttributeUsage(
+ global::System.AttributeTargets.Method |
+ global::System.AttributeTargets.Property |
+ global::System.AttributeTargets.Parameter,
+ AllowMultiple = false,
+ Inherited = false)]
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ internal sealed class UnscopedRefAttribute : global::System.Attribute
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/EquatableArray{T}.cs b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/EquatableArray{T}.cs
new file mode 100644
index 0000000..8f738f0
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/EquatableArray{T}.cs
@@ -0,0 +1,169 @@
+// 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;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Runtime.CompilerServices;
+
+namespace ReactiveUI.SourceGenerators.Helpers;
+
+///
+/// An immutable, equatable array. This is equivalent to but with value equality support.
+///
+/// The type of values in the array.
+///
+/// Creates a new instance.
+///
+/// The input to wrap.
+internal readonly struct EquatableArray(ImmutableArray array) : IEquatable>, IEnumerable
+ where T : IEquatable
+{
+ ///
+ /// The underlying array.
+ ///
+ private readonly T[]? _array = Unsafe.As, T[]?>(ref array);
+
+ ///
+ /// Gets a value indicating whether the current array is empty.
+ ///
+ public bool IsEmpty
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => AsImmutableArray().IsEmpty;
+ }
+
+ ///
+ /// Gets a reference to an item at a specified position within the array.
+ ///
+ /// The index of the item to retrieve a reference to.
+ /// A reference to an item at a specified position within the array.
+ public ref readonly T this[int index]
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => ref AsImmutableArray().ItemRef(index);
+ }
+
+ ///
+ /// Implicitly converts an to .
+ ///
+ /// An instance from a given .
+ public static implicit operator EquatableArray(ImmutableArray array) => FromImmutableArray(array);
+
+ ///
+ /// Implicitly converts an to .
+ ///
+ /// An instance from a given .
+ public static implicit operator ImmutableArray(in EquatableArray array) => array.AsImmutableArray();
+
+ ///
+ /// Checks whether two values are the same.
+ ///
+ /// The first value.
+ /// The second value.
+ /// Whether and are equal.
+ public static bool operator ==(in EquatableArray left, in EquatableArray right) => left.Equals(right);
+
+ ///
+ /// Checks whether two values are not the same.
+ ///
+ /// The first value.
+ /// The second value.
+ /// Whether and are not equal.
+ public static bool operator !=(in EquatableArray left, in EquatableArray right) => !left.Equals(right);
+
+ ///
+ /// Creates an instance from a given .
+ ///
+ /// The input instance.
+ /// An instance from a given .
+ public static EquatableArray FromImmutableArray(ImmutableArray array) => new(array);
+
+ ///
+ /// Equalses the specified array.
+ ///
+ /// The array.
+ /// A bool.
+#pragma warning disable RCS1168 // Parameter name differs from base name
+ public bool Equals(EquatableArray array) => AsSpan().SequenceEqual(array.AsSpan());
+#pragma warning restore RCS1168 // Parameter name differs from base name
+
+ ///
+ /// Equalses the specified object.
+ ///
+ /// The object.
+ /// A bool.
+ public override bool Equals([NotNullWhen(true)] object? obj) => obj is EquatableArray array && Equals(this, array);
+
+ ///
+ /// Gets the hash code.
+ ///
+ /// and int.
+ public override int GetHashCode()
+ {
+ if (_array is not T[] array)
+ {
+ return 0;
+ }
+
+ HashCode hashCode = default;
+
+ foreach (var item in array)
+ {
+ hashCode.Add(item);
+ }
+
+ return hashCode.ToHashCode();
+ }
+
+ ///
+ /// Gets an instance from the current .
+ ///
+ /// The from the current .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ImmutableArray AsImmutableArray() => Unsafe.As>(ref Unsafe.AsRef(in _array));
+
+ ///
+ /// Returns a wrapping the current items.
+ ///
+ /// A wrapping the current items.
+ public ReadOnlySpan AsSpan() => AsImmutableArray().AsSpan();
+
+ ///
+ /// Copies the contents of this instance to a mutable array.
+ ///
+ /// The newly instantiated array.
+ public T[] ToArray() => [.. AsImmutableArray()];
+
+ ///
+ /// Gets an value to traverse items in the current array.
+ ///
+ /// An value to traverse items in the current array.
+ public ImmutableArray.Enumerator GetEnumerator() => AsImmutableArray().GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)AsImmutableArray()).GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)AsImmutableArray()).GetEnumerator();
+}
+
+///
+/// Extensions for .
+///
+#pragma warning disable SA1402 // File may only contain a single type
+internal static class EquatableArray
+#pragma warning restore SA1402 // File may only contain a single type
+{
+ ///
+ /// Creates an instance from a given .
+ ///
+ /// The type of items in the input array.
+ /// The input instance.
+ /// An instance from a given .
+ public static EquatableArray AsEquatableArray(this ImmutableArray array)
+ where T : IEquatable => new(array);
+}
diff --git a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/HashCode.cs b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/HashCode.cs
new file mode 100644
index 0000000..7bfa54c
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/HashCode.cs
@@ -0,0 +1,179 @@
+// 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.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Security.Cryptography;
+
+#pragma warning disable CS0809
+
+namespace System;
+
+///
+/// A polyfill type that mirrors some methods from on .NET 6.
+///
+#pragma warning disable CA1066 // Implement IEquatable when overriding Object.Equals
+internal struct HashCode
+#pragma warning restore CA1066 // Implement IEquatable when overriding Object.Equals
+{
+ private const uint Prime1 = 2654435761U;
+ private const uint Prime2 = 2246822519U;
+ private const uint Prime3 = 3266489917U;
+ private const uint Prime4 = 668265263U;
+ private const uint Prime5 = 374761393U;
+
+ private static readonly uint seed = GenerateGlobalSeed();
+
+ private uint _v1;
+ private uint _v2;
+ private uint _v3;
+ private uint _v4;
+ private uint _queue1;
+ private uint _queue2;
+ private uint _queue3;
+ private uint _length;
+
+ ///
+ /// Adds a single value to the current hash.
+ ///
+ /// The type of the value to add into the hash code.
+ /// The value to add into the hash code.
+ public void Add(T value) => Add(value?.GetHashCode() ?? 0);
+
+ ///
+ /// Gets the resulting hashcode from the current instance.
+ ///
+ /// The resulting hashcode from the current instance.
+ public readonly int ToHashCode()
+ {
+ var length = _length;
+ var position = length % 4;
+ var hash = length < 4 ? MixEmptyState() : MixState(_v1, _v2, _v3, _v4);
+
+ hash += length * 4;
+
+ if (position > 0)
+ {
+ hash = QueueRound(hash, _queue1);
+
+ if (position > 1)
+ {
+ hash = QueueRound(hash, _queue2);
+
+ if (position > 2)
+ {
+ hash = QueueRound(hash, _queue3);
+ }
+ }
+ }
+
+ hash = MixFinal(hash);
+
+ return (int)hash;
+ }
+#pragma warning disable CA1065
+ ///
+ [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", error: true)]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override readonly int GetHashCode() => throw new NotSupportedException();
+
+ ///
+ [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override readonly bool Equals(object? obj) => throw new NotSupportedException();
+#pragma warning restore CA1065
+
+ ///
+ /// Rotates the specified value left by the specified number of bits.
+ /// Similar in behavior to the x86 instruction ROL.
+ ///
+ /// The value to rotate.
+ /// The number of bits to rotate by.
+ /// Any value outside the range [0..31] is treated as congruent mod 32.
+ /// The rotated value.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static uint RotateLeft(uint value, int offset) => (value << offset) | (value >> (32 - offset));
+
+ ///
+ /// Initializes the default seed.
+ ///
+ /// A random seed.
+ private static unsafe uint GenerateGlobalSeed()
+ {
+ var bytes = new byte[4];
+
+ using (var generator = RandomNumberGenerator.Create())
+ {
+ generator.GetBytes(bytes);
+ }
+
+ return BitConverter.ToUInt32(bytes, 0);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void Initialize(out uint v1, out uint v2, out uint v3, out uint v4)
+ {
+ v1 = seed + Prime1 + Prime2;
+ v2 = seed + Prime2;
+ v3 = seed;
+ v4 = seed - Prime1;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static uint Round(uint hash, uint input) => RotateLeft(hash + (input * Prime2), 13) * Prime1;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static uint QueueRound(uint hash, uint queuedValue) => RotateLeft(hash + (queuedValue * Prime3), 17) * Prime4;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static uint MixState(uint v1, uint v2, uint v3, uint v4) => RotateLeft(v1, 1) + RotateLeft(v2, 7) + RotateLeft(v3, 12) + RotateLeft(v4, 18);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static uint MixEmptyState() => seed + Prime5;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static uint MixFinal(uint hash)
+ {
+ hash ^= hash >> 15;
+ hash *= Prime2;
+ hash ^= hash >> 13;
+ hash *= Prime3;
+ hash ^= hash >> 16;
+
+ return hash;
+ }
+
+ private void Add(int value)
+ {
+ var val = (uint)value;
+ var previousLength = _length++;
+ var position = previousLength % 4;
+
+ if (position == 0)
+ {
+ _queue1 = val;
+ }
+ else if (position == 1)
+ {
+ _queue2 = val;
+ }
+ else if (position == 2)
+ {
+ _queue3 = val;
+ }
+ else
+ {
+ if (previousLength == 3)
+ {
+ Initialize(out _v1, out _v2, out _v3, out _v4);
+ }
+
+ _v1 = Round(_v1, _queue1);
+ _v2 = Round(_v2, _queue2);
+ _v3 = Round(_v3, _queue3);
+ _v4 = Round(_v4, val);
+ }
+ }
+}
diff --git a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/ImmutableArrayBuilder{T}.cs b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/ImmutableArrayBuilder{T}.cs
new file mode 100644
index 0000000..c1fa09f
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/ImmutableArrayBuilder{T}.cs
@@ -0,0 +1,230 @@
+// 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.Buffers;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+
+namespace ReactiveUI.SourceGenerators.Helpers;
+
+///
+/// A helper type to build sequences of values with pooled buffers.
+///
+/// The type of items to create sequences for.
+internal ref struct ImmutableArrayBuilder
+{
+ ///
+ /// The rented instance to use.
+ ///
+ private Writer? _writer;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The target data writer to use.
+ private ImmutableArrayBuilder(Writer writer) => _writer = writer;
+
+ ///
+ /// Gets the data written to the underlying buffer so far, as a .
+ ///
+ [UnscopedRef]
+ public readonly ReadOnlySpan WrittenSpan
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _writer!.WrittenSpan;
+ }
+
+ ///
+ /// Gets the count.
+ ///
+ ///
+ /// The count.
+ ///
+ public readonly int Count
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _writer!.Count;
+ }
+
+ ///
+ /// Creates a value with a pooled underlying data writer.
+ ///
+ /// A instance to write data to.
+ public static ImmutableArrayBuilder Rent() => new(new Writer());
+
+ ///
+ public readonly void Add(T item) => _writer!.Add(item);
+
+ ///
+ /// Adds the specified items to the end of the array.
+ ///
+ /// The items to add at the end of the array.
+ public readonly void AddRange(scoped in ReadOnlySpan items) => _writer!.AddRange(items);
+
+ ///
+ public readonly ImmutableArray ToImmutable()
+ {
+ var array = _writer!.WrittenSpan.ToArray();
+
+ return Unsafe.As>(ref array);
+ }
+
+ ///
+ public readonly T[] ToArray() => _writer!.WrittenSpan.ToArray();
+
+ ///
+ /// Gets an instance for the current builder.
+ ///
+ /// An instance for the current builder.
+ ///
+ /// The builder should not be mutated while an enumerator is in use.
+ ///
+ public readonly IEnumerable AsEnumerable() => _writer!;
+
+ ///
+ public override readonly string ToString() => _writer!.WrittenSpan.ToString();
+
+ ///
+ public void Dispose()
+ {
+ var writer = _writer;
+
+ _writer = null;
+
+ writer?.Dispose();
+ }
+
+ ///
+ /// A class handling the actual buffer writing.
+ ///
+ private sealed class Writer : ICollection, IDisposable
+ {
+ ///
+ /// The underlying array.
+ ///
+ private T?[]? _array;
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Creates a new instance with the specified parameters.
+ ///
+ public Writer()
+ {
+ _array = ArrayPool.Shared.Rent(typeof(T) == typeof(char) ? 1024 : 8);
+ Count = 0;
+ }
+
+ ///
+ public int Count
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get; private set;
+ }
+
+ ///
+ public ReadOnlySpan WrittenSpan
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => new(_array!, 0, Count);
+ }
+
+ ///
+ bool ICollection.IsReadOnly => true;
+
+ ///
+ public void Add(T item)
+ {
+ EnsureCapacity(1);
+
+ _array![Count++] = item;
+ }
+
+ ///
+ public void AddRange(in ReadOnlySpan items)
+ {
+ EnsureCapacity(items.Length);
+
+ items.CopyTo(_array.AsSpan(Count)!);
+
+ Count += items.Length;
+ }
+
+ ///
+ public void Dispose()
+ {
+ var array = _array;
+
+ _array = null;
+
+ if (array is not null)
+ {
+ ArrayPool.Shared.Return(array, clearArray: typeof(T) != typeof(char));
+ }
+ }
+
+ ///
+ void ICollection.Clear() => throw new NotSupportedException();
+
+ ///
+ bool ICollection.Contains(T item) => throw new NotSupportedException();
+
+ ///
+ void ICollection.CopyTo(T[] array, int arrayIndex) => Array.Copy(_array!, 0, array, arrayIndex, Count);
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ var array = _array!;
+ var length = Count;
+
+ for (var i = 0; i < length; i++)
+ {
+ yield return array[i]!;
+ }
+ }
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this).GetEnumerator();
+
+ ///
+ bool ICollection.Remove(T item) => throw new NotSupportedException();
+
+ ///
+ /// Ensures that has enough free space to contain a given number of new items.
+ ///
+ /// The minimum number of items to ensure space for in .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void EnsureCapacity(int requestedSize)
+ {
+ if (requestedSize > _array!.Length - Count)
+ {
+ ResizeBuffer(requestedSize);
+ }
+ }
+
+ ///
+ /// Resizes to ensure it can fit the specified number of new items.
+ ///
+ /// The minimum number of items to ensure space for in .
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private void ResizeBuffer(int sizeHint)
+ {
+ var minimumSize = Count + sizeHint;
+
+ var oldArray = _array!;
+ var newArray = ArrayPool.Shared.Rent(minimumSize);
+
+ Array.Copy(oldArray, newArray, Count);
+
+ _array = newArray;
+
+ ArrayPool.Shared.Return(oldArray, clearArray: typeof(T) != typeof(char));
+ }
+ }
+}
diff --git a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Diagnostics/CodeFixers/PropertyToReactiveFieldAnalyzer.cs b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Diagnostics/CodeFixers/PropertyToReactiveFieldAnalyzer.cs
index 0600275..f3b0764 100644
--- a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Diagnostics/CodeFixers/PropertyToReactiveFieldAnalyzer.cs
+++ b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Diagnostics/CodeFixers/PropertyToReactiveFieldAnalyzer.cs
@@ -10,6 +10,7 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
+using ReactiveUI.SourceGenerators.Extensions;
using static ReactiveUI.SourceGenerators.CodeFixers.Diagnostics.DiagnosticDescriptors;
namespace ReactiveUI.SourceGenerators.CodeFixers;
@@ -47,6 +48,30 @@ public override void Initialize(AnalysisContext context)
private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
+ var symbol = context.ContainingSymbol;
+ if (symbol is not IPropertySymbol propertySymbol)
+ {
+ return;
+ }
+
+ // Make sure the property is part of a class inherited from ReactiveObject
+ if (!propertySymbol.IsTargetTypeValid())
+ {
+ return;
+ }
+
+ // Check if the property is an readonly property
+ if (propertySymbol.SetMethod == null)
+ {
+ return;
+ }
+
+ // Check if the property is a ReactiveUI property
+ if (propertySymbol.GetAttributes().Any(a => a.AttributeClass?.Name == "ReactiveAttribute" || a.AttributeClass?.Name == "ObservableAsProperty"))
+ {
+ return;
+ }
+
if (context.Node is not PropertyDeclarationSyntax propertyDeclaration)
{
return;
diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs
index 6b0ad5c..49f245b 100644
--- a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs
+++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs
@@ -233,6 +233,14 @@ public TestViewModel()
[JsonInclude]
public string? TestAutoProperty { get; set; } = "Test, should prompt to replace with INPC Reactive Property";
+ ///
+ /// Gets the test read only property.
+ ///
+ ///
+ /// The test read only property.
+ ///
+ public string? TestReadOnlyProperty { get; } = "Test, should not prompt to replace with INPC Reactive Property";
+
///
/// Gets or sets the reactive command test property. Should not prompt to replace with INPC Reactive Property.
///
diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewWpf.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewWpf.cs
index c215ab2..c4e63b2 100644
--- a/src/ReactiveUI.SourceGenerators.Execute/TestViewWpf.cs
+++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewWpf.cs
@@ -19,4 +19,12 @@ public partial class TestViewWpf : Window
/// Initializes a new instance of the class.
///
public TestViewWpf() => ViewModel = TestViewModel.Instance;
+
+ ///
+ /// Gets or sets the test property.
+ ///
+ ///
+ /// The test property.
+ ///
+ public int TestProperty { get; set; }
}
diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/AnalyzerReleases.Shipped.md b/src/ReactiveUI.SourceGenerators.Roslyn/AnalyzerReleases.Shipped.md
index defa1f9..c7fce7a 100644
--- a/src/ReactiveUI.SourceGenerators.Roslyn/AnalyzerReleases.Shipped.md
+++ b/src/ReactiveUI.SourceGenerators.Roslyn/AnalyzerReleases.Shipped.md
@@ -20,6 +20,8 @@ RXUISG0013 | ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator | Error |
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
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
## Rules
@@ -76,3 +78,8 @@ This rule checks if there are any Properties to change to Reactive Field, change
- RXUISG0017 - ObservableAsPropertyFromObservableGenerator
This rule checks if the `ObservableAsProperty` has Invalid generated property declaration.
+- RXUISG0018 - ObservableAsPropertyFromObservableGenerator
+This rule checks if the `ObservableAsProperty` has Invalid class inheritance, must inherit from `ReactiveObject`.
+
+- RXUISG0018 - ReactiveGenerator
+This rule checks if the `Reactive` has Invalid class inheritance, must inherit from `ReactiveObject`.
diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/FieldSyntaxExtensions.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/FieldSyntaxExtensions.cs
index ffb47ac..e9b9780 100644
--- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/FieldSyntaxExtensions.cs
+++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/FieldSyntaxExtensions.cs
@@ -142,4 +142,18 @@ internal static bool IsTargetTypeValid(this IPropertySymbol propertySymbol)
return isIObservableObject || isObservableObject || hasObservableObjectAttribute;
}
+
+ ///
+ /// Validates the containing type for a given field being annotated.
+ ///
+ /// The input instance to process.
+ /// Whether or not the containing type for is valid.
+ internal static bool IsTargetTypeValid(this IMethodSymbol methodSymbol)
+ {
+ var isObservableObject = methodSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.ReactiveObject");
+ var isIObservableObject = methodSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.IReactiveObject");
+ var hasObservableObjectAttribute = methodSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("ReactiveUI.SourceGenerators.ReactiveObjectAttribute");
+
+ return isIObservableObject || isObservableObject || hasObservableObjectAttribute;
+ }
}
diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/DiagnosticDescriptors.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/DiagnosticDescriptors.cs
index 4cc1604..04ddbab 100644
--- a/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/DiagnosticDescriptors.cs
+++ b/src/ReactiveUI.SourceGenerators.Roslyn/Diagnostics/DiagnosticDescriptors.cs
@@ -138,4 +138,17 @@ internal static class DiagnosticDescriptors
isEnabledByDefault: true,
description: "The method annotated with [ObservableAsProperty] cannot currently initialize methods with parameters.",
helpLinkUri: "https://www.reactiveui.net/docs/handbook/view-models/boilerplate-code.html");
+
+ ///
+ /// The invalid reactive object error.
+ ///
+ public static readonly DiagnosticDescriptor InvalidReactiveObjectError = new DiagnosticDescriptor(
+ id: "RXUISG0018",
+ title: "Invalid class, does not inherit ReactiveObject",
+ messageFormat: "The field {0}.{1} cannot be used to generate an ReactiveUI property, as it is not part of a class that inherits from ReactiveObject",
+ category: typeof(ReactiveGenerator).FullName,
+ defaultSeverity: DiagnosticSeverity.Error,
+ 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");
}
diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.Execute.cs
index 701ae59..2fa9fa5 100644
--- a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.Execute.cs
+++ b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.Execute.cs
@@ -45,7 +45,7 @@ public sealed partial class ObservableAsPropertyGenerator
if (!fieldSymbol.IsTargetTypeValid())
{
builder.Add(
- InvalidObservableAsPropertyError,
+ InvalidReactiveObjectError,
fieldSymbol,
fieldSymbol.ContainingType,
fieldSymbol.Name);
diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs
index 11080b5..662a649 100644
--- a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs
+++ b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs
@@ -47,6 +47,18 @@ public sealed partial class ObservableAsPropertyGenerator
if (context.TargetNode is MethodDeclarationSyntax methodSyntax)
{
var methodSymbol = (IMethodSymbol)symbol!;
+
+ // Validate the target type
+ if (!methodSymbol.IsTargetTypeValid())
+ {
+ diagnostics.Add(
+ InvalidReactiveObjectError,
+ methodSymbol,
+ methodSymbol.ContainingType,
+ methodSymbol.Name);
+ return new(default, diagnostics.ToImmutable());
+ }
+
if (methodSymbol.Parameters.Length != 0)
{
diagnostics.Add(
@@ -106,6 +118,17 @@ public sealed partial class ObservableAsPropertyGenerator
return default;
}
+ // Validate the target type
+ if (!propertySymbol.IsTargetTypeValid())
+ {
+ diagnostics.Add(
+ InvalidReactiveObjectError,
+ propertySymbol,
+ propertySymbol.ContainingType,
+ propertySymbol.Name);
+ return new(default, diagnostics.ToImmutable());
+ }
+
var observableType = string.Empty;
var isNullableType = false;
@@ -141,7 +164,7 @@ public sealed partial class ObservableAsPropertyGenerator
if (!propertySymbol.IsTargetTypeValid())
{
diagnostics.Add(
- InvalidObservableAsPropertyError,
+ InvalidReactiveObjectError,
propertySymbol,
propertySymbol.ContainingType,
propertySymbol.Name);
diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Reactive/ReactiveGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Reactive/ReactiveGenerator.Execute.cs
index f75c55f..f6537f1 100644
--- a/src/ReactiveUI.SourceGenerators.Roslyn/Reactive/ReactiveGenerator.Execute.cs
+++ b/src/ReactiveUI.SourceGenerators.Roslyn/Reactive/ReactiveGenerator.Execute.cs
@@ -50,7 +50,7 @@ public sealed partial class ReactiveGenerator
if (!propertySymbol.IsTargetTypeValid())
{
builder.Add(
- InvalidReactiveError,
+ InvalidReactiveObjectError,
propertySymbol,
propertySymbol.ContainingType,
propertySymbol.Name);
@@ -134,7 +134,7 @@ public sealed partial class ReactiveGenerator
if (!fieldSymbol.IsTargetTypeValid())
{
builder.Add(
- InvalidReactiveError,
+ InvalidReactiveObjectError,
fieldSymbol,
fieldSymbol.ContainingType,
fieldSymbol.Name);