ReactiveProperty icon indicating copy to clipboard operation
ReactiveProperty copied to clipboard

Support for nullable reference types

Open runceel opened this issue 4 years ago • 21 comments

  • [x] Update csharp lang version to 10.0
  • [x] Enable nullable
  • [ ] Fix all errors and warnings
  • [ ] Run all unit tests with all green results

Analyzer

  • [ ] ReactivePropertySlim constructor
  • [ ] ToReadOnlyReactivePropertySlim

runceel avatar Mar 03 '21 11:03 runceel

Any movement on this?

Sod-Almighty avatar Oct 20 '21 23:10 Sod-Almighty

This was harder than I thought, so I was focusing other issues.
I will try it after releasing .net 6.

runceel avatar Oct 22 '21 00:10 runceel

I have no idea to add nullability for that...

class ReactiveProperty<T>
{
    public T Value 
    { 
        get; 
        set; 
    }

    public ReactiveProperty() : this(default)
    {
    }

    public ReactiveProperty(T initialValue)
    {
        Value = initialValue;
    }
}

runceel avatar Nov 09 '21 15:11 runceel

In My Opinion, There is no way to avoid big breaking changes.

class ReactiveProperty<T>
{
    public T Value { get; set; }

//    public ReactiveProperty() : this(default){}

    public ReactiveProperty(T initialValue)
    {
        Value = initialValue;
    }
}

usage

//public ReactiveProperty<string> RName { get; } = new(); //compile error
public ReactiveProperty<string> RName { get; } = new(string.Empty);
public ReactiveProperty<string?> RName { get; } = new(default(string));

soi013 avatar Dec 03 '21 07:12 soi013

@soi013 Thank you for the comment. I totally agree your suggestion. However, as you writing, it is a breaking change.

runceel avatar Dec 03 '21 08:12 runceel

I considered another way.

How about using a partial nullable disablle and an Analyzer?

Here is the Analyzer repository and nupkg that I made as a demo.

https://github.com/soi013/ReactivePropertyAnalyzer/ https://github.com/soi013/ReactivePropertyAnalyzer/releases/tag/v1.0

This Analyzer also works with the current version of ReactiveProperty, and shows warnings where it is initialized with null.

sc 2021-12-10 01 19 28

Partial Disabling nullable

class ReactiveProperty<T>
{
    public T Value { get; set; }

#nullable disable 
    public ReactiveProperty() : this(default){}
#nullable restore

    public ReactiveProperty(T initialValue)
    {
        Value = initialValue;
    }
}

soi013 avatar Dec 09 '21 16:12 soi013

That's great!! This is a best way to add nullability support with keeping compatibility.

runceel avatar Dec 11 '21 23:12 runceel

I hadn't thought of that approach. It's very nice!!

xin9le avatar Dec 12 '21 17:12 xin9le

@soi013 I cloned the repository, and I tried it. It is really interesting. I just added a few lines to improve the analyzer.

private void AnalyzeObjectCreationSyntax(SyntaxNodeAnalysisContext context)
{
    if (context.Node is not ObjectCreationExpressionSyntax invocationExpr)
        return;

    if (invocationExpr.Type is not GenericNameSyntax geneSynatx)
        return;

    if (geneSynatx.Identifier.ValueText is not nameof(ReactiveProperty) or nameof(ReadOnlyReactivePropertySlim))
        return;

    var typeGenerigArg = geneSynatx.TypeArgumentList.Arguments.FirstOrDefault();

    if (typeGenerigArg == null)
        return;

    var symbolGeneric = context.SemanticModel.GetSymbolInfo(typeGenerigArg).Symbol as INamedTypeSymbol;

    if (symbolGeneric == null) return;
    if (symbolGeneric.IsValueType != false) return;
    if (typeGenerigArg is NullableTypeSyntax) return; // add

    var arguments = invocationExpr.ArgumentList?.Arguments;

    if (arguments?.Count != 0)
        return;

    var diagnostic = Diagnostic.Create(Rule, invocationExpr.GetLocation(), invocationExpr.ToString());
    context.ReportDiagnostic(diagnostic);
}

private void AnalyzeImplicitObjectCreationSyntax(SyntaxNodeAnalysisContext context)
{
    if (context.Node is not ImplicitObjectCreationExpressionSyntax invocationExpr)
        return;

    var typeSymbol = context.SemanticModel.GetTypeInfo(invocationExpr, context.CancellationToken).Type;

    if (typeSymbol?.Name is not nameof(ReactiveProperty) or nameof(ReadOnlyReactivePropertySlim))
        return;

    var symbolGeneric = (typeSymbol as INamedTypeSymbol)?.TypeArguments.FirstOrDefault();

    if (symbolGeneric?.IsValueType != false)
        return;

    if (symbolGeneric.NullableAnnotation == NullableAnnotation.Annotated) return; // add

    var arguments = invocationExpr.ArgumentList?.Arguments;
    if (arguments?.Count != 0)
        return;

    var diagnostic = Diagnostic.Create(Rule, invocationExpr.GetLocation(), invocationExpr.ToString());
    context.ReportDiagnostic(diagnostic);
}

I succeeded detecting nullability.

image

I will work for supporting nullablity for ReactiveProperty. But it is still too hard. 😓

runceel avatar Dec 27 '21 02:12 runceel

LGTM!

But how to analyze of ReadOnlyReactiveProperty ? I have not any idea.

var source1 = new ReactiveProperty<string>("initialValue");
var roName1 = source1.ToReadOnlyReactiveProperty<string>(); //.Value is not null

var source2 = new ReactiveProperty<string>("initialValue", mode: ReactivePropertyMode.None);
var roNameNull = source2.ToReadOnlyReactiveProperty<string>(); //.Value is null

var roName2 = source2.ToReadOnlyReactiveProperty<string>("initialValue"); //.Value is not null

soi013 avatar Dec 27 '21 06:12 soi013

😥

runceel avatar Dec 27 '21 08:12 runceel

I also have not...

runceel avatar Dec 27 '21 08:12 runceel

This task is not interesting for me. So, please expect delay for this. If anyone interest it, please feel free fork and pullrequest for this.

runceel avatar Jan 11 '22 01:01 runceel

@runceel any update on this one? Is it still low on your priority list?

gmkado avatar Apr 19 '23 22:04 gmkado

@gmkado I had added nullable annotations for minimum necessary usecases. If you are facing warnings of nullable reference. Please let me know.

runceel avatar Apr 20 '23 09:04 runceel

Hi @runceel , thanks for responding. Should the following interactive script give me compiler warnings?

#r "nuget: ReactiveProperty, 9.1.2"
#nullable enable

using Reactive.Bindings;

public class Foo
{
	public IReadOnlyReactiveProperty<string> Bar => _bar;
	private readonly ReactivePropertySlim<string> _bar = new();  // <-- should this complain?
}

var x = new Foo();
x.Bar.Value.Contains("uh oh");

gmkado avatar Apr 20 '23 17:04 gmkado

@gmkado This is one of the problems that I could not solve. Due to historical reasons, the ReactiveProperty class has a default constructor. However, when using this default constructor, the Value property becomes null in the case of reference types. Ideally, a warning should be issued, but I am not aware of a solution to address this issue. If you know of any solution, please let me know so that I can incorporate it.

runceel avatar Apr 27 '23 01:04 runceel

@runceel I've never worked with CodeAnalyzer's before, but could you add a similar rule to the invocation of ToReadOnlyReactiveProperty?

Alternatively, if you wanted to phase out the default constructor you could mark it as [Obsolete], with a message like "Provide an initial value to the constructor". You could overload the ToReadOnlyReactiveProperty extension method instead of having the optional initialValue and mark the one without the initialValue argument as [Obsolete] as well.

gmkado avatar May 25 '23 23:05 gmkado

Thank you for your opinion. We had tried the analyzer solution, but there are a lot of patterns to detect nullable reference errors, so it was so difficult...

And I don't want to add [Obsolete] attribute for this. Because I don't want to write new ReactiveProperty<string?>(null);, I just want to write new ReactiveProperty<string?>();. Adding [Obsolete] attribute to the default constructor would make for a bad developer experience, I think.

I expect that the C# compiler will be improved in the future to detect more nullable warnings correctly.

runceel avatar May 29 '23 05:05 runceel

but there are a lot of patterns to detect nullable reference errors, so it was so difficult...

Are you referring to this?

If so, is there any harm in adding the analyzer that catches this scenario? It may not catch 100% of null references, but it would definitely help catch most of them.

Or are there more corner cases that you ran into that aren't in this thread?

gmkado avatar May 29 '23 07:05 gmkado

@gmkado

Are you referring to https://github.com/runceel/ReactiveProperty/issues/241#issuecomment-1001372776?

Yes, it is.

Or are there more corner cases that you ran into that aren't in this thread?

I am not inclined to implement this as I would originally want the C# compiler to detect it. If someone can make a PR for this, I can merge it and publish it to NuGet.

runceel avatar Jun 07 '23 09:06 runceel