From fb7a0beeb1f6454ad01117036dc56ea7e23e41bd Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sat, 1 Feb 2025 22:49:19 +0000 Subject: [PATCH] Update Code Fix to report more accurately --- .../Core/Extensions/FieldSyntaxExtensions.cs | 39 +++ .../Core/Extensions/ISymbolExtensions.cs | 33 +++ .../Core/Extensions/ITypeSymbolExtensions.cs | 122 ++++++++++ ...ics.CodeAnalysis.NotNullWhenAttribute.g.cs | 29 +++ ...ics.CodeAnalysis.UnscopedRefAttribute.g.cs | 44 ++++ .../Core/Helpers/EquatableArray{T}.cs | 169 +++++++++++++ .../Core/Helpers/HashCode.cs | 179 ++++++++++++++ .../Core/Helpers/ImmutableArrayBuilder{T}.cs | 230 ++++++++++++++++++ .../PropertyToReactiveFieldAnalyzer.cs | 25 ++ .../TestViewModel.cs | 8 + .../TestViewWpf.cs | 8 + .../AnalyzerReleases.Shipped.md | 7 + .../Core/Extensions/FieldSyntaxExtensions.cs | 14 ++ .../Diagnostics/DiagnosticDescriptors.cs | 13 + ...eAsPropertyGenerator{FromField}.Execute.cs | 2 +- ...opertyGenerator{FromObservable}.Execute.cs | 25 +- .../Reactive/ReactiveGenerator.Execute.cs | 4 +- 17 files changed, 947 insertions(+), 4 deletions(-) create mode 100644 src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Extensions/FieldSyntaxExtensions.cs create mode 100644 src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Extensions/ISymbolExtensions.cs create mode 100644 src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Extensions/ITypeSymbolExtensions.cs create mode 100644 src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Generated/PolySharp.SourceGenerators/PolySharp.SourceGenerators.PolyfillsGenerator/System.Diagnostics.CodeAnalysis.NotNullWhenAttribute.g.cs create mode 100644 src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Generated/PolySharp.SourceGenerators/PolySharp.SourceGenerators.PolyfillsGenerator/System.Diagnostics.CodeAnalysis.UnscopedRefAttribute.g.cs create mode 100644 src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/EquatableArray{T}.cs create mode 100644 src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/HashCode.cs create mode 100644 src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/ImmutableArrayBuilder{T}.cs 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);