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

(4/4) Prefer 'AsSpan' over 'Substring' analyzer #4806

Conversation

NewellClark
Copy link
Contributor

@NewellClark NewellClark commented Feb 9, 2021

Fixes issue #33784.
Violations found in dotnet/runtime:

  • System.Private.DataContractSerialization (PR)
  • System.Private.Xml (PR)

Edit: ambiguous cases are now reported, but not fixed.

I'm marking this as a draft for now because I wanted to ask what we should do when only some of the Substring calls can be converted to AsSpan (currently it only reports when all Substring invocations in the call can be converted).

using Roschar = System.ReadOnlySpan<char>
// No overload takes 3 roschars
public void Consume(string a, string b, string c) { }
public void Consume(Roschar a, string b, string c) { }
public void Consume(string a, Roschar b, Roschar c) { }

public void NotAmbiguous(string foo)
{
    Consume(foo.Substring(1), foo.Substring(2), foo.Substring(3));

    // Candidates
    Consume(foo.AsSpan(1), foo.Substring(2), foo.Substring(3));    // Option 1
    Consume(foo.Substring(1), foo.AsSpan(2), foo.AsSpan(3));       // Option 2
}

Should we mirror overload resolution and have the fixer choose the "best" option here? Seems like the right thing to do. But what about when it's ambiguous?

using Roschar = System.ReadOnlySpan<char>;

public void Consume(string left, string right) { }
public void Consume(Roschar left, string right) { }
public void Consume(string left, Roschar right) { }

public void Ambiguous(string foo)
{
    Consume(foo.Substring(1), foo.Substring(2));

    // Candidates
    Consume(foo.AsSpan(1), foo.Substring(2));    // Option 1
    Consume(foo.Substring(1), foo.AsSpan(2));    // Option 2
 }

When it's ambiguous, should we not report at all? Or should we report, but not fix?

Add 'GetFirstOrDefaultMemberWithParameterTypes' helper method
@NewellClark NewellClark requested a review from a team as a code owner February 9, 2021 05:44
@NewellClark NewellClark marked this pull request as draft February 9, 2021 05:44
@codecov
Copy link

codecov bot commented Feb 9, 2021

Codecov Report

Merging #4806 (34dc2d6) into release/6.0.1xx (92a247f) will increase coverage by 0.01%.
The diff coverage is 97.14%.

@@                 Coverage Diff                 @@
##           release/6.0.1xx    #4806      +/-   ##
===================================================
+ Coverage            95.58%   95.59%   +0.01%     
===================================================
  Files                 1215     1220       +5     
  Lines               278104   279856    +1752     
  Branches             16768    16815      +47     
===================================================
+ Hits                265824   267529    +1705     
- Misses               10037    10077      +40     
- Partials              2243     2250       +7     

@sharwell
Copy link
Member

sharwell commented Feb 9, 2021

Is this a common case?

I would typically prefer one of the following:

  • Report a diagnostic and provide two possible fixes
  • Report a diagnostic but do not provide a fix

@NewellClark
Copy link
Contributor Author

  • Report a diagnostic but do not provide a fix

Sounds reasonable. I don't think it's super common, so I'll do this for now, unless we find evidence that it's a common scenario.

Previously, fixer would only fix cases where all Substring calls could be converted. Now, it fixes all cases where there is a single unambiguous best overload.
- Handle cases where not every Substring invocation can be converted to AsSpan
- Add tests for inaccessible overloads
@NewellClark
Copy link
Contributor Author

@sharwell I've implemented it so that ambiguous cases are reported, but not fixed.

Fix issue where 'System' namespace would be imported unnecessarily when violations were inside a System namespace declaration, or where the System import was inside a namespace declaration. 

This issue was encountered in dotnet/runtime
private protected override void ReplaceInvocationMethodName(SyntaxEditor editor, SyntaxNode memberInvocation, string newName)
{
var cast = (InvocationExpressionSyntax)memberInvocation;
var memberAccessSyntax = (MemberAccessExpressionSyntax)cast.Expression;
Copy link
Member

Choose a reason for hiding this comment

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

Are we sure we can't be on something else than MemberAccessExpresionSyntax here? Maybe a member binding or something like this.

Copy link
Contributor Author

@NewellClark NewellClark Feb 19, 2021

Choose a reason for hiding this comment

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

The only way this could be a MemberBindingExpressionSyntax is if the substring invocation is conditional, foo?.Substring(1).
This can't possibly be true because the analyzer detects IArgumentOperations that are IInvocationOperations (after stripping implicit conversions), and the syntax of foo?.Substring(1) is a ConditionalAccessExpressionSyntax, and not an InvocationExpressionSyntax.
I'll add some tests ensuring the analyzer does not report conditional substring invocations, and I'll add some assertions and comments to document this invariant.
I'll also rename the method to ReplaceNonConditionalInvocationMethodName


context.RegisterOperationBlockStartAction(OnOperationBlockStart);

void OnOperationBlockStart(OperationBlockStartAnalysisContext context)
Copy link
Member

Choose a reason for hiding this comment

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

[minor] Method could be made static.

Copy link
Contributor Author

@NewellClark NewellClark Feb 19, 2021

Choose a reason for hiding this comment

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

It captures symbols (used on line 63).

Copy link
Member

Choose a reason for hiding this comment

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

My bad, it's not easy to see captured variables in github.

}
}

internal static IOperation WalkDownImplicitConversions(IOperation operation)
Copy link
Member

Choose a reason for hiding this comment

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

I am wondering if we should update the WalkDownConversions helper to have an optional predicate function Func<IConversionOperation, bool>.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that would be a good idea. I've wanted that function on several other analyzers. While we're talking about upgrading helpers, I've also wanted a GetFirstOrDefaultWithParamater overload that takes ITypeSymbols instead of ParameterInfos.

-Add tests ensuring conditional substring invocations are not reported
-Add separate resource for code fix title
-Remove period from method name
-Inline diagnostic descriptors
-Add WalkDownConversion overload that takes a predicate.
-Use more descriptive names for RequiredSymbols members.
@@ -12,7 +12,7 @@
Design: CA2210, CA1000-CA1070
Globalization: CA2101, CA1300-CA1310
Mobility: CA1600-CA1601
Performance: HA, CA1800-CA1838
Performance: HA, CA1800-CA1842
Copy link
Member

Choose a reason for hiding this comment

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

Why the jump from 38 to 42?

Copy link
Contributor Author

@NewellClark NewellClark Feb 19, 2021

Choose a reason for hiding this comment

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

Because my other analyzer PRs use CA1839-CA1841. I wasn't sure how it's typically handled when there are multiple outstanding PRs that both want to add an analyzer to the same diagnostic category.

Copy link
Member

Choose a reason for hiding this comment

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

Makes sense, maybe you could add a [X/Y] prefix on your PRs so that we merge them in the right order.

Copy link
Contributor Author

@NewellClark NewellClark Feb 19, 2021

Choose a reason for hiding this comment

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

I've added prefixes here (1/4), here (2/4), here (3/4), and this one is 4/4. I think some of them may be in different milestones. Do you want me to change the IDs around so that earlier milestones get merged first?

/// <param name="operation">The starting operation.</param>
/// <param name="predicate">A predicate to filter conversion operations.</param>
/// <returns>The first operation that either isn't a conversion or doesn't satisfy <paramref name="predicate"/>.</returns>
public static IOperation WalkDownConversion(this IOperation operation, Func<IConversionOperation, bool> predicate)
Copy link
Member

Choose a reason for hiding this comment

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

Maybe this could be merged with the previous and allow nullable Func. Note that I am not convinced whether it brings much.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did end up needing it on one of my other PRs. I can see it being useful in the future, especially for dealing with arguments where you don't want to skip through explicit casts.

@Evangelink
Copy link
Member

@sharwell @mavasani Could one of you review this PR? Thank you.

Co-authored-by: Amaury Levé <amaury.leve@gmail.com>
@NewellClark NewellClark changed the title Prefer 'AsSpan' over 'Substring' analyzer (D) Prefer 'AsSpan' over 'Substring' analyzer Feb 19, 2021
@NewellClark NewellClark changed the title (D) Prefer 'AsSpan' over 'Substring' analyzer (4/4) Prefer 'AsSpan' over 'Substring' analyzer Feb 19, 2021
@NewellClark NewellClark changed the base branch from release/6.0.1xx-preview2 to release/6.0.1xx-preview4 May 8, 2021 13:44
}
}

private static IEnumerable<IMethodSymbol> GetAllAccessibleOverloadsIncludingSelf(IInvocationOperation invocation, CancellationToken cancellationToken)
Copy link
Contributor

@mavasani mavasani May 19, 2021

Choose a reason for hiding this comment

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

Can we use/enhance/update this extension method?

public static IEnumerable<IMethodSymbol> GetOverloads(this IMethodSymbol? method)

Copy link
Contributor Author

@NewellClark NewellClark May 19, 2021

Choose a reason for hiding this comment

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

That method is poorly named. Basically, it looks at a particular invocation operation and finds all overloads of the invoked method that would also be legal at that location, taking into account the type of the receiver (for instance methods) or type (for static methods). It's pretty specific to this analyzer; I can't make a version that uses an IMethodSymbol instead of an IInvocationOperation because it depends on the receiver of the invocation. I'll rename it to something more descriptive to make this clearer.

If we did want to add an extension method that does this, it couldn't go in IMethodSymbolExtensions because the extended receiver would need to be IInvocationOperation.

Edit: I've renamed the method to GetAllAccessibleOverloadsAtInvocationCallSite to reduce confusion.

[MemberData(nameof(Data_NestedViolations))]
public Task NestedViolations_AreAllReportedAndFixed_CS(
string receiverClass, string testExpression, string fixedExpression, int[] locations,
int? incrementalIterations = null, int? fixAllInDocumentIterations = null, int? fixAllIterations = null)
Copy link
Contributor

Choose a reason for hiding this comment

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

All the yield return values in Data_NestedViolations seems to provide non-null values for these 3 parameters. Can these parameters be marked non-optional?

Copy link
Contributor

Choose a reason for hiding this comment

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

Also it seems fixAllInDocumentIterations and fixAllIterations is always 1. Do we need a parameter for these as all?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think at one point I had different values for these parameters. I'll mark them non-optional and remove fixAllInDocumentIterations and fixAllIterations, as they are always 1.

public void Consume(string a, Roschar b) { }";
yield return new[]
{
CS.WithBody(WithKey(@"Consume(foo.Substring(1), foo.Substring(2))", 0) + ';', members)
Copy link
Contributor

Choose a reason for hiding this comment

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

We can potentially consider showing 2 code fixes here, one for each best candidate, but given this would be a rare case, current approach of not showing code fix is also reasonable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mavasani I'm happy to implement that functionality. How do I write tests that expect two code fixes for one violation? Is there another analyzer that does this that I could look at as an example?

Copy link
Contributor

Choose a reason for hiding this comment

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

@sharwell Might have suggestions on this - I don't believe we need this as part of this PR. Feel free to file a separate issue or open a follow-up PR for it.

Copy link
Contributor

@mavasani mavasani left a comment

Choose a reason for hiding this comment

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

Overall looks great to me and extensive unit tests give a lot of confidence, thanks!

Have some minor comments - I can merge once they are resolved.

NewellClark and others added 2 commits May 20, 2021 10:25
Co-authored-by: Manish Vasani <mavasani@microsoft.com>
@mavasani mavasani self-assigned this May 20, 2021
@mavasani
Copy link
Contributor

@NewellClark Can you please resolve merge conflicts?

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

Successfully merging this pull request may close these issues.

4 participants