Skip to content

"Add null check" inserts single line statements if preferred#52462

Merged
sharwell merged 13 commits intodotnet:mainfrom
MaStr11:SingleLineAddNullCheck
Apr 8, 2021
Merged

"Add null check" inserts single line statements if preferred#52462
sharwell merged 13 commits intodotnet:mainfrom
MaStr11:SingleLineAddNullCheck

Conversation

@MaStr11
Copy link
Contributor

@MaStr11 MaStr11 commented Apr 7, 2021

Fixes #52385

@MaStr11 MaStr11 requested a review from a team as a code owner April 7, 2021 10:28
@ghost ghost added the Area-IDE label Apr 7, 2021
@MaStr11
Copy link
Contributor Author

MaStr11 commented Apr 7, 2021

It seems, csharp_prefer_braces is already respected in CodeAction.PostProcessChangesAsync. Braces are removed by Simplifier.ReduceAsync here:

if (document.SupportsSyntaxTree)
{
document = await ImportAdder.AddImportsFromSymbolAnnotationAsync(
document, Simplifier.AddImportsAnnotation, cancellationToken: cancellationToken).ConfigureAwait(false);
document = await Simplifier.ReduceAsync(document, Simplifier.Annotation, cancellationToken: cancellationToken).ConfigureAwait(false);
// format any node with explicit formatter annotation
document = await Formatter.FormatAsync(document, Formatter.Annotation, cancellationToken: cancellationToken).ConfigureAwait(false);
// format any elastic whitespace
document = await Formatter.FormatAsync(document, SyntaxAnnotation.ElasticAnnotation, cancellationToken: cancellationToken).ConfigureAwait(false);
document = await CaseCorrector.CaseCorrectAsync(document, CaseCorrector.Annotation, cancellationToken).ConfigureAwait(false);
}

Afterwards the document looks like this (syntaxRoot.ToFullString()):

using System;

class C
{
    public C(object o)
    {
if(oisnull)thrownew ArgumentNullException(nameof(o));    }
}

The next change happens in Formatter.FormatAsync(SyntaxAnnotation.ElasticAnnotation) afterwards it looks like this:

using System;

class C
{
    public C(object o)
    {
        if (o is null)
            throw new ArgumentNullException(nameof(o));
    }
}

It seems csharp_style_allow_embedded_statements_on_same_line_experimental is not respected by Formatter.FormatAsync(document, SyntaxAnnotation.ElasticAnnotation).

The propable cause is the IndentBlockFormattingRule.AddEmbeddedStatementsIndentationOperation

private static void AddEmbeddedStatementsIndentationOperation(List<IndentBlockOperation> list, SyntaxNode node)
{
// increase indentation - embedded statement cases
if (node is IfStatementSyntax ifStatement && ifStatement.Statement != null && !(ifStatement.Statement is BlockSyntax))
{
AddEmbeddedStatementsIndentationOperation(list, ifStatement.Statement);
return;
}

It does not respect csharp_style_allow_embedded_statements_on_same_line_experimental. This option is also not available in IndentBlockFormattingRule.CachedOptions

private readonly struct CachedOptions : IEquatable<CachedOptions>
{
public readonly LabelPositionOptions LabelPositioning;
public readonly bool IndentBlock;
public readonly bool IndentSwitchCaseSection;
public readonly bool IndentSwitchCaseSectionWhenBlock;
public readonly bool IndentSwitchSection;

@CyrusNajmabadi Am I on the right track here. Should IndentBlockFormattingRule be changed to respect csharp_style_allow_embedded_statements_on_same_line_experimental?

@CyrusNajmabadi
Copy link
Contributor

csharp_style_allow_embedded_statements_on_same_line_experimental is not respected by Formatter.FormatAsync(document,

'Allow' does not mean 'require'. It just means it is on if the user does that.

The fix is to check for both of these and not have elastic trivia after the close paren or before the throw keyword. Thanks!

@MaStr11
Copy link
Contributor Author

MaStr11 commented Apr 7, 2021

'Allow' does not mean 'require'. It just means it is on if the user does that.

But shouldn't Formatter.FormatAsync(SyntaxAnnotation.ElasticAnnotation) respect csharp_style_allow_embedded_statements_on_same_line_experimental?

The fix is to check for both of these and not have elastic trivia after the close paren or before the throw keyword. Thanks!

I tried to find the SyntaxAnnotation.ElasticAnnotation on any of the descendants nodes and tokens of the generated if statement but couldn't find any. So I think the Formatter.FormatAsync(SyntaxAnnotation.ElasticAnnotation) needs to be touched, or do I miss something?

@MaStr11
Copy link
Contributor Author

MaStr11 commented Apr 7, 2021

Okay. I found the ElasticAnnotations in the trivia. Overall there are 49 such annotations (empty whitespace with the elastic marker). Do I really need to remove it? The problem is that the statement syntax tree is generated in a language neutral manner, so I can't influence the ElasticAnnotation placement there:

private static TStatementSyntax CreateNullCheckStatement(SemanticModel semanticModel, SyntaxGenerator generator, IParameterSymbol parameter)
=> (TStatementSyntax)generator.CreateNullCheckAndThrowStatement(semanticModel, parameter);

@CyrusNajmabadi
Copy link
Contributor

But shouldn't Formatter.FormatAsync(SyntaxAnnotation.ElasticAnnotation) respect csharp_style_allow_embedded_statements_on_same_line_experimental?

No. Because the general pattern in the ecosystem is to still place them on multiple lines. This is jsut a way for clients to say if it's should be considered illegal to be on the same line. Not that it is preferred that they be on the same line. They're very different concepts.

@CyrusNajmabadi
Copy link
Contributor

Do I really need to remove it?

Yes. i would not include the trivia between those two tokens. :) I wouldn't remove them after the fact, i woudl just not add them in the first place.

@MaStr11
Copy link
Contributor Author

MaStr11 commented Apr 7, 2021

Yes. i would not include the trivia between those two tokens. :) I wouldn't remove them after the fact, i woudl just not add them in the first place.

I finally got this approach working. 0f157af shows that the close brace and the throw keyword need both trivia handling to avoid the line break/indentation. I think the way forward here is to move away from SyntaxGenerator and use abstract methods in AbstractAddParameterCheckCodeRefactoringProvider instead. Am I right, or should we add overloads for trivia handling?

@jinujoseph jinujoseph added the Community The pull request was submitted by a contributor who is not a Microsoft employee. label Apr 7, 2021
@CyrusNajmabadi
Copy link
Contributor

I think the way forward here is to move away from SyntaxGenerator and use abstract methods in AbstractAddParameterCheckCodeRefactoringProvider instead. Am I right, or should we add overloads for trivia handling?

This seems reasonable.

@MaStr11 MaStr11 changed the title "Add null check" inserts single line statements if prefferd "Add null check" inserts single line statements if preferred Apr 8, 2021
@MaStr11
Copy link
Contributor Author

MaStr11 commented Apr 8, 2021

@CyrusNajmabadi and @sharwell Ready for review.

Copy link
Contributor

@sharwell sharwell left a comment

Choose a reason for hiding this comment

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

The implementation seems fine. I would like to see new tests added for the "Fix All" case before merging.

{
{ CSharpCodeStyleOptions.PreferBraces, new CodeStyleOption2<PreferBracesPreference>((PreferBracesPreference)preferBraces, NotificationOption2.Silent) }
{ CSharpCodeStyleOptions.PreferBraces, new CodeStyleOption2<PreferBracesPreference>((PreferBracesPreference)preferBraces, NotificationOption2.Silent) },
{ CSharpCodeStyleOptions.AllowEmbeddedStatementsOnSameLine, false },
Copy link
Contributor

Choose a reason for hiding this comment

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

💭 Considering this test was previously showing the behavior for a defaulted value of AllowEmbeddedStatementsOnSameLine, it might be preferable to change FixedCode instead of changing this line.

It makes me wonder if we should have used FalseWithSilentEnforcement as the default for AllowEmbeddedStatementsOnSameLine, but if that changes it can be a separate change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 7205621


[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsInitializeParameter)]
[WorkItem(52385, "https://github.com/dotnet/roslyn/issues/52385")]
public async Task SingleLineStatement_NullCheck_BracesNone_SameLineFalse()
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Can you also add some tests for using code action equivalence key Add_null_checks_for_all_parameters (e.g. based on TestMultiNullableObjects)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 0318ce5

End Function

Protected Overrides Function CreateParameterCheckIfStatement(options As DocumentOptionSet, condition As ExpressionSyntax, ifTrueStatement As StatementSyntax) As StatementSyntax
Return SyntaxFactory.MultiLineIfBlock(
Copy link
Contributor

Choose a reason for hiding this comment

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

💭 Technically we could do the same thing here, but I'm not sure it's common enough to need to. I'd be fine leaving it as a follow-up, especially considering AllowEmbeddedStatementsOnSameLine would need to be moved to the shared options first.

Copy link
Contributor

Choose a reason for hiding this comment

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

agreed.

fwiw, we don't check AllowEmbeddedStatementsOnSameLine in vb at all. it was really intended for the C# case (and may just be a c# option currently). we have very little use of this pattern in vb.

SyntaxFactory.IfStatement(SyntaxFactory.Token(SyntaxKind.IfKeyword), condition, SyntaxFactory.Token(SyntaxKind.ThenKeyword)),
New SyntaxList(Of StatementSyntax)(ifTrueStatement),
Nothing,
Nothing)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: named parameters plz.

Copy link
Contributor Author

@MaStr11 MaStr11 Apr 8, 2021

Choose a reason for hiding this comment

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

Done in a557a4a

Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi left a comment

Choose a reason for hiding this comment

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

Thanks! @jnm2 will appreciate this a lot :)

@sharwell sharwell enabled auto-merge April 8, 2021 18:57
@sharwell sharwell merged commit e5fa846 into dotnet:main Apr 8, 2021
@ghost ghost added this to the Next milestone Apr 8, 2021
@MaStr11 MaStr11 deleted the SingleLineAddNullCheck branch April 9, 2021 06:43
@dibarbet dibarbet modified the milestones: Next, 16.10.P3 Apr 26, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-IDE Community The pull request was submitted by a contributor who is not a Microsoft employee.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support single-line null checks for 'Add null check'

5 participants