Giter VIP home page Giter VIP logo

generator.equals's Issues

Cleanup IEquatable<T> implementation for classes

This would require a breaking change, so I am adding this to the 3.0 milestone.

The idea would be to make the method IEquatable<T>.Equals(T) virtual and have a similar implementation to records, where the immediate base is overridden and sealed, calling Equals(object?). Below is the decompiled source of a derived record -- that's what we want for classes.

    [NullableContext(2)]
    [CompilerGenerated]
    public override bool Equals(object obj)
    {
      return this.Equals(obj as Derived);
    }

    [NullableContext(2)]
    [CompilerGenerated]
    public override sealed bool Equals(Base other)
    {
      return this.Equals((object) other);
    }

    [NullableContext(2)]
    [CompilerGenerated]
    public virtual bool Equals(Derived other)
    {
      if ((object) this == (object) other)
        return true;
      return base.Equals((Base) other) && EqualityComparer<string>.Default.Equals(this.\u003CName\u003Ek__BackingField, other.\u003CName\u003Ek__BackingField);
    }

Indexers on classes lead to syntactically invalid generated code

First of: Very useful generator. Thanks for building this! :)

I ran into a small issue with classes that have indexers:

[Equatable]
partial class Sample
{
    public string Property { get; set; }
    
    public string this[int index] => index.ToString();
}

The generated code looks like this: (shortened for readability)

partial class Sample : IEquatable<Sample> {

// ...

public bool Equals(Sample? other) {
return !ReferenceEquals(other, null) && this.GetType() == other.GetType()
&& EqualityComparer<String>.Default.Equals(Property!, other.Property!)
&& EqualityComparer<String>.Default.Equals(this!, other.this!) // <- invalid
;
}

// ...

public override int GetHashCode() {
                var hashCode = new global::System.HashCode();
            
hashCode.Add(this.GetType());
hashCode.Add(this.Property!, EqualityComparer<String>.Default);
hashCode.Add(this.this!, EqualityComparer<String>.Default); // <- invalid
return hashCode.ToHashCode();
}

which is then rejected by the compiler with CS1001 and CS1003.

It seems that Generator.Equals thinks that this is the name of a property here, rather than interpreting it as the keyword indicating an indexer.

As a workaround, I can mark the indexer with [IgnoreEquality]. However, I think it would be better if Generator.Equals simply ignored indexers by default.

DictionaryEquality is redundant

We can juse use UnorderedSequenceEquality, since IDictionary<TKey, TValue> extends IEnumerable<KeyValuePair<TKey, TValue>>, and KeyValuePair<TKey, TValue> implements Equals and HashCode correctly.

using Generator.Equals;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;

namespace Test
{
    [Equatable]
    partial record MyRecord(
        [property: UnorderedSequenceEquality] Dictionary<string, int> Mutable,
        [property: UnorderedSequenceEquality] ImmutableDictionary<string, int> Immutable
    );

    class Program
    {
        static void Main()
        {
            var mutable1 = new Dictionary<string, int> { { "A", 0 }, { "B", 42 } };
            var mutable2 = new Dictionary<string, int> { { "A", 0 }, { "B", 42 } };

            var record1 = new MyRecord(mutable1, mutable1.ToImmutableDictionary());
            var record2 = new MyRecord(mutable2, mutable2.ToImmutableDictionary());

            Console.WriteLine(record1 == record2);
        }
    }
}

Explicit mode

Hi. Cool generator.
I am migrating to your generator from this - https://github.com/tom-englert/Equatable.Fody. It uses Fody and explicitly defined properties to implement IEquatable. I would like to see the same mode in this generator, in which you need to explicitly specify the necessary properties.
It would also be great to think about supporting IIncrementalGenerator

`IEnumerable<T>` property marked by `[UnorderedEquality]` causes exception during generation

I've recently found this package and wanted to replace own reflection-based equality. I marked up classes but got no result. Building project I've got warning:

CSC : warning CS8785: Generator 'EqualsGenerator' failed to generate source. It will not contribute to the output and compilation errors may occur as a result. Exception was of type 'InvalidOperationException' with message 'Nullable object must have a value'

So, error message wasn't explanatory enough. After quite a bit of debugging, I've found out that my class contained IEnumerable<T> property with UnorderedEquality attribute. It caused NullReferenceException during generation.

I believe, it is a bug, isn't it? If so, I can provide some fix.

P.S. Thanks for your work

Support StringEqualityAttribute on collections of strings

While experimenting with Generator.Equals, I found that if I try to customize string equality to be case insensitive for a property that is for example an array of strings, the code generated by Generator.Equals will currently incorrectly assume the property is a string. GetHashCode code produces a compilation error:

CS0411: The type arguments for method 'HashCode.Add(T, IEqualityComparer?)' cannot be inferred from the usage. Try specifying the type arguments explicitly.

I assume the only way Generator.Equals can handle this case is via a custom equality, but I believe the case to be common enough to consider supporting it directly. Making StringEqualityAttribute handle it seems like it would be nice. Other than that, in general it would be nice if generation failed with a better error when an attribute is used with a member of an unsupported type.

Moreover, if I try to combine StringEqualityAttribute and UnorderedEqualityAttribute on the same property, generation will obey unordered equality and the intent to treat the string elements as case insensitive will be ignored. Personally I think this is also a compelling scenario to support.

Repro

using Generator.Equals;

Console.WriteLine("Hello, World!");

[Equatable]
public partial class Resource
{
    [StringEquality(StringComparison.OrdinalIgnoreCase)]
    public string[] Tags { get; set; } = Array.Empty<string>();

}

Generator.Equals will detect the attribute and generate code that assumes that the property is a string, for both the code generated for equality and the code generated for hash code:

    /// <inheritdoc/>
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Generator.Equals", "1.0.0.0")]
    protected bool Equals(global::Resource? other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        
        return other.GetType() == this.GetType()
            && global::System.StringComparer.OrdinalIgnoreCase.Equals(this.Tags!, other.Tags!)
            ;
    }
    
    /// <inheritdoc/>
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Generator.Equals", "1.0.0.0")]
    public override int GetHashCode()
    {
        var hashCode = new global::System.HashCode();
        
        hashCode.Add(this.GetType());
        hashCode.Add(
            this.Tags!,
            global::System.StringComparer.OrdinalIgnoreCase
        );
        
        return hashCode.ToHashCode();
    }

Feature Request: Would be great to be able to enumerate the properties that are different!

Would be great to have a wrapper class such that one can have a Comparer collect the difference between say two record's or other supported type. This would also walk the nested collection and say use a ., [x] or => notation for the hierarchy, array/set elements or maps.

A Use Case: CDC you have the before and after image from your database of choice and need detect what changes happen between them, so some conditional biz logic can be applied based upon that.

That this generator avoids reflection means it will also work with AoT compilation.

Compiler-error with static properties

Classes with static properties, like:

[Equatable]
partial class Sample
{
    public string Property { get; set; }
    
    public static string StaticProperty { get; set; }
}

lead to a compiler error when used with Generator.Equals:

Member 'Sample.StaticProperty' cannot be accessed with an instance reference; qualify it with a type name instead

As a workaround, I can mark static properties with [IgnoreEquality]. However, I think it would be better if Generator.Equals simply ignored them by default.

Missing XML comments on generated code

Hey,

If you're using XML doc comments and have warnings enabled when they are missing the source generator gets picked up (even though you've correctly added GeneratedCodeAttribute which you'd expect to be skipped but isn't).

Would you be interested in either:

  • Generate inheritdoc comments for overridden methods and default comments for operators
  • Add #pragma warning disable 1591 to the file to avoid the warnings entirely?

I'd be happy to contribute a PR for either if it's welcomed ๐Ÿ‘

Inherited classes Equals ignored when using OrderedEquality on an array of the base class

This is actually a follow up to #38 but in another situation.

Problem

The ObjectEqualityComparer<T> class is used, which actually calls x.Equals(y) where y is of the expected time, resulting in only the base class Equals implementation being called.

I'm wondering if marking the typed Equals method as public might make things harder? If it was protected instead, it would be much harder to fall in that trap. The method checks for

Reproduction

public class GeneratorBaseClassBugTests
{
  [Equatable]
  public partial class MyContainer
  {
    [OrderedEquality]
    public MyBase[]? Content { get; set; }
  }
  
  [Equatable]
  public abstract partial class MyBase
  {
  }
  
  [Equatable]
  public partial class MyImpl : MyBase
  {
    public int Value { get; set; }
  }

  [Test]
  public void TestEqual()
  {
    var v1 = new MyContainer { Content = new MyBase[] { new MyImpl { Value = 1 } } };
    var v2 = new MyContainer { Content = new MyBase[] { new MyImpl { Value = 1 } } };

    Assert.That(v1.Equals(v2), Is.True);
  }

  [Test]
  public void TestNotEqual()
  {
    var v1 = new MyContainer { Content = new MyBase[] { new MyImpl { Value = 1 } } };
    var v2 = new MyContainer { Content = new MyBase[] { new MyImpl { Value = 2 } } };

    Assert.That(v1.Equals(v2), Is.False);
  }
}

Solution

The code ends up calling SequenceEqual, which again calls GenericEqualityComparer<BaseType>, ignoring inherited Equals implementations. This is actually the behavior of SequenceEqual, which makes me think that even outside of your comparison code, the generated equality checks may fail in other circumstances. Let me know if you believe going the protected route for the typed Equals implementation sounds like the way to go.

Version

.NET 6.0, Generator.Equals 2.7.2

Sorry to be a pain, hopefully that'll avoid some nasty surprises for other people :D Thanks again for your consideration and the great library!

Support for Record Struct

[Equatable]
public partial record struct MyStruct(int Data);

Generates

partial record MyStruct
{
    /// <inheritdoc/>
    [GeneratedCode("Generator.Equals", "1.0.0.0")]
    public bool Equals(MyStruct? other)
    {
        return
            base.Equals(other)
         && EqualityComparer<Int32>.Default.Equals(Data!, other.Data!)
            ;
    }
        
    /// <inheritdoc/>
    [GeneratedCode("Generator.Equals", "1.0.0.0")]
    public override int GetHashCode()
    {
        var hashCode = new HashCode();
            
        hashCode.Add(base.GetHashCode());
        hashCode.Add(
            Data!,
            EqualityComparer<Int32>.Default);
            
        return hashCode.ToHashCode();
    }
}

This is correct except it should say partial record struct MyStruct and the Equals method should be

/// <inheritdoc />
    [GeneratedCode("Generator.Equals", "1.0.0.0")]
    public bool Equals(MyStruct? other)
    {
        return
            other.HasValue
         && EqualityComparer<Int32>.Default.Equals(Data, other.Value.Data)
            ;
    }

also the GetHashCode method does not need to do hashCode.Add(base.GetHashCode());

I am willing to have a go at this myself.

No Licence?

Hey there,

I was just wondering if you intended this library to not be licenced since that implies that all rights are reserved and it can't redistribute your code without explict permissions.

Support for struct

Hello,

Would it be possible to also generate equality members for struct ?

Support for Reference Equality

There might be some niche cases where you want this:

    public class ObjectReferenceEqualityComparer<T> : IEqualityComparer<T>
        where T : class
    {
        private static IEqualityComparer<T> _defaultComparer;
        public static IEqualityComparer<T> Default => _defaultComparer ?? (_defaultComparer = new ObjectReferenceEqualityComparer<T>());

        bool IEqualityComparer<T>.Equals(T x, T y)
        {
            return ReferenceEquals(x, y);
        }

        int IEqualityComparer<T>.GetHashCode(T obj)
        {
            return RuntimeHelpers.GetHashCode(obj);
        }
    }

Remove the runtime dependency

Another idea is to remove the runtime dependency, especially for those who don't use custom Comparers. They can be delivered explicitly (with global:: full typepaths to avoid possible collisions).

Originally posted by @HavenDV in #22 (comment)

What if properties of a class are also generated by another code generator?

Hi! Thank you for this awesome library.

Suppose I have a ViewModel class like this:

using System.IO;
using System.Windows.Media.Imaging;

using CommunityToolkit.Mvvm.ComponentModel;
using Dapper.Contrib.Extensions;
using Generator.Equals;

namespace CoilQueryTool.ViewModels
{
    [Equatable(IgnoreInheritedMembers = true)]
    [Table("CoilMap")]
    internal partial class CoilRecordViewModel : ObservableObject
    {
        public CoilRecordViewModel ShallowCopy()
        {
            return (CoilRecordViewModel)MemberwiseClone();
        }

        [IgnoreEquality]
        public int Id { get; set; }

        [ObservableProperty]
        private string coilId = null!;

        [ObservableProperty]
        private string? name;

        [ObservableProperty]
        private string? connector;

        [ObservableProperty]
        private string? pN;

        [ObservableProperty]
        private string? description;

        [ObservableProperty]
        private string? machine;

        [ObservableProperty]
        private string? mode;
    }
}

Since most of the properties of this class is generated by another code generator - CommunityToolkit.Mvvm's code generator, Generator.Equals will not generate code for these properties.

I am wondering if [DefaultEquality] can be used like this:

class MyViewModel
{
    [DefaultEquality]
    [ObservableProperty]
    private string title;
}

Do not require runtime reference to Generator.Equals.Runtime.dll

Even for a simple usage like this

    [Equatable]
    internal partial class Class1
    {
        public string Text { get; set; } = string.Empty;
    }

a runtime reference to Generator.Equals.Runtime.dll is required, just because of the usage of the attribute.

Proposed solution:

Decorate the attributes with e.g. [Conditional("GENERATOR_EQUALS")] (like e.g. JetBrains does for their annotation attributes: https://www.jetbrains.com/help/resharper/Code_Analysis__Annotations_in_Source_Code.html)

This way a runtime reference should only be needed when using one of the special comparators.

NullReferenceExceptions starting with v2.7.2

Iโ€™ve been using your lib for a while in 0install-dotnet. Great project, thanks!

Starting with v2.7.2 up to and including v2.7.5 my builds targeting .NET Framework 4.7.2 (with <Nullable>annotations</Nullable>) are throwing exceptions for null values:

System.NullReferenceException with message "Object reference not set to an instance of an object."
   at Generator.Equals.DefaultEqualityComparer`1.ObjectEqualityComparer.GetHashCode(T obj)
   at System.HashCode.Add[T](T value, IEqualityComparer`1 comparer)
   at ZeroInstall.Model.Command.GetHashCode() in /_/src/Model/Generator.Equals/Generator.Equals.EqualsGenerator/ZeroInstall.Model.Command.Generator.Equals.g.cs:line 58

The same code targeting .NET 6.0 (with <Nullable>enable</Nullable>) still works fine.

Was this an unintentional change or do I need to change something in my usage of the lib?

Suppress obsolete warnings in generated code

Issue
When I mark a property as obsolete but don't use [IgnoreEquality], then I get build warnings about usages of the obsolete property.

I would expect those to be suppressed because they will go away when the property is removed anyway. In the meantime, I can't use [Obsolete] on properties in projects where I treat warnings as errors.

Solution 1
Use a pragma to suppress build warnings when comparing obsolete things in the generated source.

Solution 2
Add a toggle on [Equatable] to decide whether to use a pragma to suppress obsolete warnings. Something like:

[Equatable(SuppressObsoleteWarnings = true)]
public partial record Something
{
  public string Thing1 { get; set; }
  [Obsolete]
  public string Thing2 { get; set; }
}

If you have a preferred way of doing it, I'd be more than happy to write up a PR.

Compatibility with the mvvm community toolkit

This code will generate equality comparisons:
[DefaultEquality] public bool MyProperty{ get; set; }
or
[DefaultEquality] private bool myProperty;

But the following will not generate any equality code:

[ObservableProperty]
[property: DefaultEquality]
private bool myProperty;

and this will generate the code but it also causes warning message MVVMTK0034:

[ObservableProperty]
[DefaultEquality] 
private bool isFromExistingStock;

Reasoning: some libraries (like the MVVM community toolkit) require all attributes to be applied to the backing field, but doing so will introduce warnings whenever you try to access the backing field instead of the property.

I reviewed the source generated by MVVM toolkit, and it appears that (when using [property:DefaultEquality] the auto-generated property will include the DefaultEqualityAttribute, but source code normally generated by Generator.Equals is not included. Maybe attributes generated a source generator cannot be used to generate more code? It would make sense if that was the issue.

/// <inheritdoc cref="myProperty"/>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::Generator.Equals.DefaultEqualityAttribute()]
public bool MyProperty
{
    get => myProperty;
    set
    {
        if (!global::System.Collections.Generic.EqualityComparer<bool>.Default.Equals(myProperty, value))
        {
            OnMyPropertyChanging(value);
            OnMyPropertyChanging(default, value);
            OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.MyProperty);
            myProperty = value;
            OnMyPropertyChanged(value);
            OnMyPropertyChanged(default, value);
            OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.MyProperty);
        }
    }
}

Thoughts?

README contains an uncompilable code

using Generator.Equals;

[Equatable]
partial class MyClass
{
    [SequenceEquality] 
    public string[] Fruits { get; set; }
);

The last line should be };.

Replace MSTest and NUnit with Xunit.

Xunit works really well with nullable by using ctor and IDisposable for its setup/teardown semantics. Need to refactor tests to use that and also adopt snapshot tests throughout.

Inherited classes Equals ignored when called from the base class

First of all, great library, thanks for making it!

Problem

When calling .Equals() or System.Collections.Generic.EqualityComparer on a base class that uses [Equatable], the generated Equals implementation will not call inherited Equals implementations.

  • When the base class doesn't use [Equatable], reference equality is used (which is not what we want)
  • When the base class uses [Equatable], the base class only checks it's own fields and ignores inherited types fields.

Reproduction

public class GeneratorBaseClassBugTests
{
  [Equatable]
  public partial class MyContainer
  {
	  public MyBase Content { get; set; }
  }
  
  [Equatable]
  public abstract partial class MyBase
  {
  }
  
  [Equatable]
  public partial class MyImpl : MyBase
  {
	  public int Value { get; set; }
  }

  [Test]
  public void Test()
  {
    var v1 = new MyContainer { Content = new MyImpl { Value = 1 } };
    var v2 = new MyContainer { Content = new MyImpl { Value = 2 } };

    Assert.That(v1, Is.Not.EqualTo(v2));
  }
}

Solution

This code is generated in the container:

global::System.Collections.Generic.EqualityComparer<global::MyNamespace.MyBase>.Default.Equals(Content!, other.Content!)

This shorts-circuits the concrete type by directly checking the base type. This would work:

global::System.Object.Equals(Content!, other.Content!)

Version

.NET 6.0, Generator.Equals 2.6.0

Thanks!

Could not load file or assembly 'netstandard, Version=2.1.0.0,

When I run the example from README, I get printed 'False'.

Probably it has something to do with the weird warning I'm getting:

An instance of analyzer Generator.Equals.EqualsGenerator cannot be created from C:\Users\Patrik\.nuget\packages\generator.equals\0.3.0\analyzers\dotnet\cs\Generator.Equals.dll: Could not load file or assembly 'netstandard, Version=2.1.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' or one of its dependencies. The system cannot find the file specified..

Here's the code: https://github.com/PatrikBak/Bug.Generator.Equals

Visual Studio 2019 Version 16.8.1

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.