Skip to content

Add DeclareAsNullable refactoring#26661

Closed
jcouv wants to merge 1 commit intodotnet:features/NullableReferenceTypesfrom
jcouv:nullable-refactoring
Closed

Add DeclareAsNullable refactoring#26661
jcouv wants to merge 1 commit intodotnet:features/NullableReferenceTypesfrom
jcouv:nullable-refactoring

Conversation

@jcouv
Copy link
Member

@jcouv jcouv commented May 6, 2018

The purpose for this refactoring is to assist in migrating a project to use the nullable feature.
When we see a comparison of a non-nullable with null (such as nonNullable == null or nonNullable != null), there is a good chance that nonNullable should in fact be declared as nullable instead. This refactoring helps do that with one click.
Note this is only a good chance, because such null checks could also be defensive programming, especially when it comes to public APIs (you declare them as non-nullable, but check anyways).

image

From discussion with Chuck, I will switch this to an analyzer/fixer so that it's more discoverable. But I won't offer a FixAll.

@jcouv jcouv added this to the 16.0 milestone May 6, 2018
@jcouv jcouv self-assigned this May 6, 2018
@jcouv jcouv requested a review from a team as a code owner May 6, 2018 06:53
}

[Fact]
public async Task TestAlreadyNullableRefType()
Copy link
Contributor

@Neme12 Neme12 May 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "ref" sounds misleading (I thought of RefTypeSyntax), consider using ReferenceType #Pending

static string M2() => throw null;
}";

await TestMissingInRegularAndScriptAsync(code, parameters: s_nullableFeature);
Copy link
Contributor

@Neme12 Neme12 May 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider supporting this by splitting the declaration #WontFix

return;
}
}
static T M2<T>() => throw null;
Copy link
Contributor

@Neme12 Neme12 May 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: is T? not supported? #ByDesign

await TestMissingInRegularAndScriptAsync(code, parameters: s_nullableFeature);
}

private Task TestMissingInRegularAndScriptAsync(string initialMarkup, IDictionary<OptionKey, object> options)
Copy link
Contributor

@Neme12 Neme12 May 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like this isn't used anywhere #Pending

<data name="Use_explicit_type_instead_of_var" xml:space="preserve">
<value>Use explicit type instead of 'var'</value>
</data>
<data name="Declare as nullable" xml:space="preserve">
Copy link
Contributor

@Neme12 Neme12 May 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing underscores #Pending

{
return null;
}

Copy link
Contributor

@Neme12 Neme12 May 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider supporting is null and possibly even (object)x == null and ReferenceEquals #Pending

var declaration = (VariableDeclarationSyntax)declarator.Parent;
return declaration.Variables.Count == 1 ? declaration.Type : null;
}
return null;
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi May 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about things like a foreach block that doesn't have a VarDecl? #ByDesign

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a good scenario in a foreach where a null test would indicate that the iteration variable should be declared as nullable.


In reply to: 186297577 [](ancestors = 186297577)

return property.Type;

case MethodDeclarationSyntax method:
if (method.Modifiers.Any(SyntaxKind.PartialKeyword))
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi May 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why? #Pending

{
return null;
}
return method.ReturnType;
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi May 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blank line after } #Pending

return null;
}

private async Task<ISymbol> TryGetSymbolToFix(SyntaxNode root, TextSpan textSpan, Document document, CancellationToken cancellationToken)
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi May 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

returns Task, add 'Async' suffix. #Pending

Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi May 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

order of args is a bit off from normal patterns. usually we go 'wider to narrower'. so Document/Root/Span. #Pending

return null;
}

private async Task<ISymbol> TryGetSymbolToFix(SyntaxNode root, TextSpan textSpan, Document document, CancellationToken cancellationToken)
Copy link
Contributor

@Neme12 Neme12 May 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: static #Pending

Copy link
Contributor

@Neme12 Neme12 May 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and above #Pending

}
else if (token.Parent.IsKind(SyntaxKind.NullLiteralExpression) && token.Parent.IsParentKind(SyntaxKind.EqualsExpression, SyntaxKind.NotEqualsExpression))
{
equals = (BinaryExpressionSyntax)token.Parent.Parent;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be simplified by first checking for null, and walking to the parent. Then just checking == and != once.

type = method.ReturnType;
break;
case IFieldSymbol field:
type = field.Type;
Copy link
Contributor

@Neme12 Neme12 May 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: consider extracting helper to get the type, if such a helper doesn't already exist somewhere #WontFix

private static async Task<Document> UpdateDocumentAsync(Document document, TypeSyntax typeToFix, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var editor = new SyntaxEditor(root, document.Project.Solution.Workspace);
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi May 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an editor is somewhat overkill here since you ca njust do root.ReplaceNode. Editors are good when you have a lot of work to do. #Pending

public const string CodeActionsUseExpressionBody = "CodeActions.UseExpressionBody";
public const string CodeActionsUseImplicitType = "CodeActions.UseImplicitType";
public const string CodeActionsUseExplicitType = "CodeActions.UseExplicitType";
public const string CodeActionsDeclaredAsNullable = "CodeActions.DeclaredAsNullable";
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi May 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inconsistency. "Declared" here, but "Declare" elsewhere. #Pending


private static async Task<Document> UpdateDocumentAsync(Document document, TypeSyntax typeToFix, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
Copy link
Contributor

@Neme12 Neme12 May 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is already done inside ComputeRefactoringsAsync #ByDesign

private static async Task<Document> UpdateDocumentAsync(Document document, TypeSyntax typeToFix, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var editor = new SyntaxEditor(root, document.Project.Solution.Workspace);
Copy link
Contributor

@Neme12 Neme12 May 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it worth it to use the syntax editor for just 1 change? #Pending

@jcouv jcouv added the PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. label May 7, 2018
@jcouv
Copy link
Member Author

jcouv commented May 7, 2018

From discussion with Chuck, I will switch this to be an analyzer/fixer. We'd offer a suggestion on s == null. That would be more discoverable (there is a clue in the UI that you can do something there).
But I wouldn't offer a FixAll for this case, as this is not a high-confidence fix (you have to review each case to decide what the proper meaning is). #Resolved

@CyrusNajmabadi
Copy link
Contributor

CyrusNajmabadi commented May 7, 2018

How about a fix-all, but with an actual model dialog warning the user needs to confirm? #ByDesign

@jcouv
Copy link
Member Author

jcouv commented May 11, 2018

@CyrusNajmabadi Are you suggesting that the user would have to individually confirm every instance that gets fixed?

As a side note, I also wanted to ask:
The compiler is currently producing hidden diagnostics for such situations (nonNullable == null produces HDN_NullCheckIsProbablyAlwaysFalse). Is there a way an analyzer could make those visible (turn them into info or suggestion diagnostics), to provide a visual clue that a fix is available at that location? #Resolved

@CyrusNajmabadi
Copy link
Contributor

CyrusNajmabadi commented May 11, 2018

@CyrusNajmabadi Are you suggesting that the user would have to individually confirm every instance that gets fixed?

No. There would be a single notification. #Resolved

/// For example: `nonNull == null`, `nonNull?.Property`
/// </summary>
[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.DeclareAsNullable), Shared]
internal class DeclareAsNullableCodeRefactoringProvider : CodeRefactoringProvider
Copy link
Member

@alrz alrz Sep 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why a refactoring? I think fix-all is most preferable for this. I'd expect such analyzer look for all null literals and null checks (including ?., ??, ??=, etc) and offer to change the target type. Now, the said type could come from anywhere like a local declaration (if not var), parameter, return type, field, property, class base, or a type argument. The fix-all could be a little more involved since we probably should infer nullability when you pass a value to a nullable parameter (need to guard for recursions). I think it'd make more sense to get this info from the compiler (#30110?) #Closed

Copy link
Member Author

@jcouv jcouv Sep 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking for null is a weak indication that a variable might allow null. I expect many APIs to declare non-nullable parameters, but check those parameters.
it may be possible to distinguish two cases: if null then throw, and, if null do something else.
The first scenario should most likely leave the parameter as non-nullable. #Closed

@cartermp
Copy link
Contributor

cartermp commented Feb 26, 2019

Any thoughts on allowing this to also (optionally?) modify an interface implementation? Otherwise this will trigger a further warning:

interface INulls
{
    public void M(string s);
}
class Nulls : INulls
{
    public void M(string? s) // Signature after applying code fix
    { 
        if (s == null) Console.WriteLine("Oh boy it's null");
    }
}

Which you could argue is a good thing - perhaps seeing that fixing the warning involves a contract change makes you consider how not to introduce a null in the first place - but if I know what I'm doing and find it acceptable it'd be nice to not have to jump to the interface to change the signature manually. #Resolved

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

Labels

Area-IDE Feature - Nullable Reference Types Nullable Reference Types PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants