Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for nullable reference types #241

Open
2 of 6 tasks
runceel opened this issue Mar 3, 2021 · 21 comments
Open
2 of 6 tasks

Support for nullable reference types #241

runceel opened this issue Mar 3, 2021 · 21 comments

Comments

@runceel
Copy link
Owner

runceel commented Mar 3, 2021

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

Analyzer

  • ReactivePropertySlim constructor
  • ToReadOnlyReactivePropertySlim
@runceel runceel self-assigned this Mar 3, 2021
@runceel runceel linked a pull request Mar 3, 2021 that will close this issue
@Sod-Almighty
Copy link

Any movement on this?

@runceel
Copy link
Owner Author

runceel commented Oct 22, 2021

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

@runceel
Copy link
Owner Author

runceel commented Nov 9, 2021

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;
    }
}

@soi013
Copy link
Contributor

soi013 commented Dec 3, 2021

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));

@runceel
Copy link
Owner Author

runceel commented Dec 3, 2021

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

@soi013
Copy link
Contributor

soi013 commented Dec 9, 2021

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;
    }
}

@runceel
Copy link
Owner Author

runceel commented Dec 11, 2021

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

@xin9le
Copy link
Collaborator

xin9le commented Dec 12, 2021

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

@runceel
Copy link
Owner Author

runceel commented Dec 27, 2021

@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. 😓

@soi013
Copy link
Contributor

soi013 commented Dec 27, 2021

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

@runceel
Copy link
Owner Author

runceel commented Dec 27, 2021

😥

@runceel
Copy link
Owner Author

runceel commented Dec 27, 2021

I also have not...

@runceel
Copy link
Owner Author

runceel commented Jan 11, 2022

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.

@gmkado
Copy link

gmkado commented Apr 19, 2023

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

@runceel
Copy link
Owner Author

runceel commented Apr 20, 2023

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

@gmkado
Copy link

gmkado commented Apr 20, 2023

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");

@runceel
Copy link
Owner Author

runceel commented Apr 27, 2023

@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.

@gmkado
Copy link

gmkado commented May 25, 2023

@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.

@runceel
Copy link
Owner Author

runceel commented May 29, 2023

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.

@gmkado
Copy link

gmkado commented May 29, 2023

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?

@runceel
Copy link
Owner Author

runceel commented Jun 7, 2023

@gmkado

Are you referring to #241 (comment)?

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants