riok / mapperly

A .NET source generator for generating object mappings. No runtime reflection.
https://mapperly.riok.app
Apache License 2.0
2.61k stars 141 forks source link

Incorrect nullable handling with implicit operators #1500

Open MatthewSmit-Scope opened 1 month ago

MatthewSmit-Scope commented 1 month ago

Describe the bug When mapping from a nullable type (either struct or class) to a type that has an implicit operator, mapperly incorrectly generates null handling checks. This occurs both when mapping directly, as in the example below, or when mapping a class with properties of this type.

As the implicit operator can take a nullable type, I would expect the nullable value to be passed in without the null check. In cases where the implicit operator can't take a nullable type (e.g., int? to TrackedProperty in the example below), the current behaviour of generating the null check is correct.

Declaration code

using Riok.Mapperly.Abstractions;

[Mapper]
public partial class Simple1
{
    public static partial TrackedProperty<int?> ToFoo(int? source);
}

[Mapper]
public partial class Simple2
{
    public static partial Dst ToFoo(Src source);
}

public class Src
{
    public int? Foo { get; set; }
}

public class Dst
{
    public TrackedProperty<int?> Foo { get; set; }
}

public readonly record struct TrackedProperty<T>
{
    private readonly T value;

    public TrackedProperty(T value)
    {
        this.value = value;
    }

    public static implicit operator TrackedProperty<T>(T value)
    {
        return new TrackedProperty<T>(value);
    }
}

Actual relevant generated code

// <auto-generated />
#nullable enable
public partial class Simple1
{
    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "4.0.0.0")]
    public static partial global::TrackedProperty<int?> ToFoo(int? source)
    {
        return source == null ? throw new System.ArgumentNullException(nameof(source)) : (global::TrackedProperty<int?>)source.Value;
    }
}

public partial class Simple2
{
    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "4.0.0.0")]
    public static partial global::Dst ToFoo(global::Src source)
    {
        var target = new global::Dst();
        if (source.Foo != null)
        {
            target.Foo = (global::TrackedProperty<int?>)source.Foo.Value;
        }
        return target;
    }
}

Expected relevant generated code

// <auto-generated />
#nullable enable
public partial class Simple1
{
    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "4.0.0.0")]
    public static partial global::TrackedProperty<int?> ToFoo(int? source)
    {
        return (global::TrackedProperty<int?>)source;
    }
}

public partial class Simple2
{
    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "4.0.0.0")]
    public static partial global::Dst ToFoo(global::Src source)
    {
        var target = new global::Dst();
        target.Foo = (global::TrackedProperty<int?>)source.Foo;
        return target;
    }
}

Environment (please complete the following information):

Additional context We have a partial workaround by declaring a user-implemented mapping, however we seem to be unable to create a generic user-implemented mapper.

    // This does not work
    public static TrackedProperty<T> UserMap<T>(T source) => source;

    // This does, but requires a separate function per type
    public static TrackedProperty<int?> UserMap(int? source) => source;
MRmlik12 commented 3 weeks ago

Is this issue in progress? If not I can handle with that

latonz commented 3 weeks ago

Not yet in progress, feel free to contribute.