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

[Analyzer Proposal]: Convert argument null checks to ArgumentNullException.ThrowIfNull #68326

Closed
stephentoub opened this issue Apr 21, 2022 · 21 comments · Fixed by dotnet/roslyn-analyzers#6293
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.Threading code-analyzer Marks an issue that suggests a Roslyn analyzer code-fixer Marks an issue that suggests a Roslyn code fixer in-pr There is an active PR which will close this issue when it is merged
Milestone

Comments

@stephentoub
Copy link
Member

In support of the now-defunct !! feature, @RikkiGibson built into a Roslyn an analyzer/fixer that found constructs like:

void M(string arg)
{
    if (arg is null)
        throw new ArgumentNullException(arg);
    ...
}

and replaced them with:

void M(string arg!!)
{
    ...
}

Now that !! is no more, we should salvage this work and transform it to instead use ArgumentNullException.ThrowIfNull if available:

void M(string arg)
{
    ArgumentNullException.ThrowIfNull(arg);
    ...
}
@stephentoub stephentoub added code-analyzer Marks an issue that suggests a Roslyn analyzer code-fixer Marks an issue that suggests a Roslyn code fixer api-ready-for-review API is ready for review, it is NOT ready for implementation labels Apr 21, 2022
@stephentoub stephentoub added this to the 7.0.0 milestone Apr 21, 2022
@dotnet-issue-labeler dotnet-issue-labeler bot added area-System.Threading untriaged New issue has not been triaged by the area owner labels Apr 21, 2022
@stephentoub stephentoub removed the untriaged New issue has not been triaged by the area owner label Apr 21, 2022
@ghost
Copy link

ghost commented Apr 21, 2022

Tagging subscribers to this area: @mangod9
See info in area-owners.md if you want to be subscribed.

Issue Details

In support of the now-defunct !! feature, @RikkiGibson built into a Roslyn an analyzer/fixer that found constructs like:

void M(string arg)
{
    if (arg is null)
        throw new ArgumentNullException(arg);
    ...
}

and replaced them with:

void M(string arg!!)
{
    ...
}

Now that !! is no more, we should salvage this work and transform it to instead use ArgumentNullException.ThrowIfNull if available:

void M(string arg)
{
    ArgumentNullException.ThrowIfNull(arg);
    ...
}
Author: stephentoub
Assignees: -
Labels:

area-System.Threading, untriaged, code-analyzer, code-fixer, api-ready-for-review

Milestone: 7.0.0

@RikkiGibson
Copy link
Contributor

I think this is a good idea, and that this would be done by adjusting the existing analyzer/fixer in the Roslyn IDE code style layer. Tagging @CyrusNajmabadi for a second opinion.

@CyrusNajmabadi
Copy link
Member

Yup.

@RikkiGibson RikkiGibson self-assigned this Apr 25, 2022
@RikkiGibson
Copy link
Contributor

I'm going to do this before removing '!!' from the compiler, since it should let us leave a bunch of the existing IDE tests and plumbing in place.

@RikkiGibson
Copy link
Contributor

RikkiGibson commented Apr 26, 2022

@CyrusNajmabadi and I chatted offline and we think that a pared-down version of the feature might be appropriate here.

We think that it might be most appropriate to handle this by:

  1. removing the 'UseParameterNullChecking' analyzer/fixer that was added for '!!' (possibly migrating the tests elsewhere).
  2. adjust the implementation of the 'UseThrowExpression' analyzer to suggest the 'ArgumentNullException.ThrowIfNull' form if a null-checked parameter is not assigned to a field after.
  3. ensure that we have separate editorconfig settings to control "prefer param ?? throw" independently from "prefer ThrowIfNull(param).
// before
public Widget(string param)
{
    if (param is null)
    {
        throw new ArgumentNullException(nameof(param));
    }
    this.param = param;
}

// after
public Widget(string param)
{
    this.param = param ?? throw new ArgumentNullException(nameof(param));
}
// before
public void M(string param)
{
    if (param is null)
    {
        throw new ArgumentNullException(nameof(param));
    }
}

// after
public void M(string param)
{
    ArgumentNullException.ThrowIfNull(param);
}

We also have concerns about the perf benefits that are suggested to be present when the user uses a helper method to throw for an uncommon case, versus using a throw directly in an uncommon code path in the same method. That's specifically the fact that methods which directly include throw can't be inlined, right?

It feels like it would be ideal to adjust the runtime so it is able to inline methods which contain throw, so that large amounts of existing code can benefit. To some extent it's concerning to ask users to adjust their coding patterns to accommodate what seems like an internal behavior detail of the runtime.

@CyrusNajmabadi
Copy link
Member

It feels like it would be ideal to adjust the runtime so it is able to inline methods which contain throw, so that large amounts of existing code can benefit.

or, alternatively, the runtime should look specifically for if (x is null) throw ArgumentNullException(nameof(x))`` and treat that identically to whatever it generates for ArgumentNullException.ThrowIfNull(x)`. Basically, it's not a pit of success for these two idiomatic expressions to have such different perf outcomes.

@Eli-Black-Work
Copy link

Can we also modify the existing "Add null check" quickfix in Visual Studio so that it has an option for generating an ArgumentNullException.ThrowIfNull call? 🙂

The "Add null check" quickfix currently looks like this:

add null check quickfix

Ideally, the quickfix would also have an option for generating a call to ArgumentNullException.ThrowIfNull, so that the resulting code looks like this:

public string Example(object name)
{
    ArgumentNullException.ThrowIfNull(name);

    return name.ToString();
}

(I originally posted this at dotnet/roslyn#61181, and someone directed me here 🙂)

@terrajobst terrajobst added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Jun 23, 2022
@terrajobst
Copy link
Member

terrajobst commented Jun 23, 2022

Video

Make sense as proposed

We feel like this would also approve the other throw helpers:

  • ObjectDisposedException.ThrowIf
  • ArgumentException.ThrowIfNullOrEmpty

@GrabYourPitchforks
Copy link
Member

Implementation nit: make sure the analyzer doesn't result in inadvertent boxing.

// no boxing
public static void DoIt<T>(T value) {
    if (value is null) { throw new ArgumentNullException(/* ... */); }
}

// boxes in all cases
public static void DoIt<T>(T value) {
    ArgumentNullException.ThrowIfNull(value);
}

@Eli-Black-Work
Copy link

Eli-Black-Work commented Jun 24, 2022

@GrabYourPitchforks I wonder if there should also be a performance analyzer that warns about user code that does that. I certainly wouldn't have thought of it 😅

@RikkiGibson
Copy link
Contributor

It might be good to check whether/when the boxing is elided by JIT in such scenarios.

@JamesNK
Copy link
Member

JamesNK commented Aug 24, 2022

aspnetcore is interested in this analyzer. We have a lot of source code with traditional null checks. We'd use the analyzer+fixer to automate updating our repo source - dotnet/aspnetcore#43482

@buyaa-n
Copy link
Contributor

buyaa-n commented Nov 15, 2022

@RikkiGibson are you planning to implement this analyzer? If so, will remove the help-wanted label

@RikkiGibson
Copy link
Contributor

Thanks for following up. I don't anticipate I will have time to work on it.

@buyaa-n
Copy link
Contributor

buyaa-n commented Nov 15, 2022

Estimates:

Analyzer: Medium
Fixer: Medium

@danmoseley
Copy link
Member

the UseParameterNullChecking analyzer/fixer, now removed, can be cribbed from https://github.com/dotnet/roslyn/pulls/58182

Would this go in the Roslyn repo, or the roslyn-analyzers repo? Not sure which is for what. I'm guessing !! was a language feature, hence it was in dotnet/roslyn.

I assume this analyzer would only handle ThrowIfNull, as others like ArgumentOutOfRangeException.ThrowIf, ObjectDisposedException.ThrowIf and the others proposed (#77749, #69590) are sufficiently different.

@stephentoub
Copy link
Member Author

Would this go in the Roslyn repo, or the roslyn-analyzers repo?

roslyn-analyzers

I assume this analyzer would only handle ThrowIfNull

Each should have its own diagnostic ID, but whether they're implemented in the same analyzer is an implementation detail.

Are you asking because you're planning to work on it?

@stephentoub stephentoub self-assigned this Nov 28, 2022
@stephentoub
Copy link
Member Author

I'm going to look at this one...

@stephentoub stephentoub removed the help wanted [up-for-grabs] Good issue for external contributors label Nov 28, 2022
@stephentoub stephentoub added the in-pr There is an active PR which will close this issue when it is merged label Dec 1, 2022
@Eli-Black-Work
Copy link

@stephentoub Sorry to bug, but did @GrabYourPitchforks's nit at #68326 (comment) get addressed? (And if not, shall I file a new bug for that?)

@stephentoub
Copy link
Member Author

The analyzer fires for generics as well. I went back and forth on whether to do so, as even though it results in boxing in the IL, the JIT will undo the boxing, at least in tier 1, e.g. SharpLab. But if we want to change the analyzer to not raise diagnostics for unconstrained or struct-constrained generic parameters, I wouldn't push back.

@Eli-Black-Work
Copy link

Got it, thanks 🙂

@ghost ghost locked as resolved and limited conversation to collaborators Jan 6, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Threading code-analyzer Marks an issue that suggests a Roslyn analyzer code-fixer Marks an issue that suggests a Roslyn code fixer in-pr There is an active PR which will close this issue when it is merged
Projects
None yet
Development

Successfully merging a pull request may close this issue.

10 participants