Detect when a range indexer is used on strings or arrays, then immediately implicitly converted to a Span or Memory type.#3464
Conversation
For each different kind of error, the "Local" variant uses the long form, and verifies the message arguments.
|
Right now the tests don't pass on my machine, because it says the fixed code doesn't compile. If not for that, the change would be ready... so I'm opening the PR to see if it's me, or the repository state. The fixer could certainly be smarter, and write prettier code.. e.g. str[3..5] => str.AsSpan(3, 2); but I couldn't figure out how to constant fold and it seemed like a nice to have on top, vs the essence of the feature. |
|
cc @dotnet/dotnet-analyzers |
|
OK, looks like it's because the tests are using the 3.0.0 version of the compiler package ( ). Upgrading it to 3.5.0 (the latest version with no hyphens in it) is now not reporting all of the diagnostics I expect... so now I'm looking into what's up with that.Thanks for the version pointer, @sharwell. |
|
@bartonjs The core analyzer packages are currently built against Microsoft.CodeAnalysis version 3.0.0 as they do not use any Roslyn APIs from a later version. This enables folks to install the analyzer packages on older compiler/VS. It should be fine to upgrade the test layer to newer Microsoft.CodeAnalysis version, we do this as and when we encounter a scenario such as this one. |
|
@jcouv did something similar for his changes that required the tests to run against latest Microsoft.CodeAnalysis: roslyn-analyzers/eng/Versions.props Line 24 in d02078f We should probably rename that property to |
|
Why so many diagnostic IDs? Why not just have one? If the goal is just to provide different messages, there are other ways. Similarly, the single rule could be configured for different types. |
That was in my intro. I believe the string version is "always right" so I have it on as a warning. Array->ReadOnly as "almost always right" and Array->Writable as "probably right". Since they had different false positive rates i went with different IDs. if there're ways to control nuance for the same ID then I'm happy with that model.. I just don't know what it looks like in code or UX. |
If you want different default severities and/or enabled by default values, using different IDs is the simplest possible approach. We can use options, but that is primarily used if user wants to control some configuration and is not always discoverable. |
src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseAsSpanInsteadOfRangeIndexer.cs
Outdated
Show resolved
Hide resolved
src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseAsSpanInsteadOfRangeIndexer.cs
Outdated
Show resolved
Hide resolved
...ers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseAsSpanInsteadOfRangeIndexerTests.cs
Outdated
Show resolved
Hide resolved
...alyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseAsSpanInsteadOfRangeIndexer.Fixer.cs
Outdated
Show resolved
Hide resolved
...alyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseAsSpanInsteadOfRangeIndexer.Fixer.cs
Outdated
Show resolved
Hide resolved
| [ExportCodeFixProvider(LanguageNames.CSharp), Shared] | ||
| public sealed class CSharpUseAsSpanInsteadOfRangeIndexerFixer : UseAsSpanInsteadOfRangeIndexerFixer | ||
| { | ||
| protected override bool TrySplitExpression( |
There was a problem hiding this comment.
I was going to suggest you to use language-agnostic IOperation based approach here, but it seems that input[3..5] actually generates an OperationKind.None node: https://github.com/dotnet/roslyn/blob/master/src/Compilers/CSharp/Test/IOperation/IOperation/IOperationTests_IFromEndIndexOperation_IRangeOperation.cs#L52
@333fred Do we have a work item tracking the IOperation implementation for this case?
There was a problem hiding this comment.
@agocke did we not actually implement Range/Index in IOperation? I don't think we blocked the master merge on that, but iirc it was expected that IOperation/CFG would be implemented shortly thereafter. dotnet/roslyn#31545
There was a problem hiding this comment.
I seem to remember a substantial amount was implemented, but we may have left it as OperationKind.None to prevent considering it "final." I don't remember what still needs to be implemented, if anything.
There was a problem hiding this comment.
I've renamed that issue to clarify that we need to confirm the implemention, not just add tests
...ers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseAsSpanInsteadOfRangeIndexerTests.cs
Outdated
Show resolved
Hide resolved
| return; | ||
| } | ||
|
|
||
| if (operationContext.Operation.Syntax.RawKind != (int)Microsoft.CodeAnalysis.CSharp.SyntaxKind.ElementAccessExpression) |
There was a problem hiding this comment.
We should not use Microsoft.CodeAnalysis.CSharp.SyntaxKind in shared layer. This will cause C# assemblies to be loaded from VB only solution. If you need to access C# specific types, you should change this type to abstract and have the C# sub-type have C# specific logic.
There was a problem hiding this comment.
It shouldn't cause an assembly load, since it's an enum (const values are burned in to the caller). If we're concerned about the ease of creep I could change it to a literal and a comment; which feels better to me than over-engineering a hierarchy here.
mavasani
left a comment
There was a problem hiding this comment.
Marking as request changes so we accidentally do not access language-specific types in language agnostic layer, which will be an assembly load problem. Overall, the PR looks good to me.
|
It looks like there's a runtime breaking change in the constructors for Microsoft.CodeAnalysis.CSharp.CSharpCompilationOptions between whatever Test.Utilities is building with and 3.5.0, so changing just the one dependency is failing: ... it looks like the namespace for NullableContextOptions changed (there's no CSharp in it anymore). Trying to figure out how to move forward from here. |
|
If at first you don't succeed... add reflection? Testing the workaround now. |
|
@bartonjs - great find! Please do file a bug on Roslyn repo for the breaking change in the constructor, which should get this fixed asap in Roslyn. Thanks! |
|
It's already known / marked as "can't fix without introducing yet another breaking change". |
Codecov Report
@@ Coverage Diff @@
## master #3464 +/- ##
==========================================
+ Coverage 95.30% 95.31% +0.01%
==========================================
Files 1027 1033 +6
Lines 233052 234250 +1198
Branches 15068 15141 +73
==========================================
+ Hits 222111 223282 +1171
- Misses 9278 9294 +16
- Partials 1663 1674 +11 |
| public abstract class CodeMetricsTestBase | ||
| { | ||
| private static readonly CompilationOptions s_CSharpDefaultOptions = new Microsoft.CodeAnalysis.CSharp.CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary); | ||
| private static readonly CompilationOptions s_CSharpDefaultOptions = BuildDefaultCSharpOptions(); |
There was a problem hiding this comment.
It will also be fine to move Test.Utilities.csproj to a newer Microsoft.CodeAnalysis reference.
Detects
ReadOnlySpan<char> slice = str[a..b];ReadOnlySpan<SomeT> slice = arr[a..b];ReadOnlyMemory<SomeT> slice = arr[a..b];Span<SomeT> slice = arr[a..b];Memory<SomeT> slice = arr[a..b];as well as using the expression as an argument to an appropriate parameter type. It would also detect
str[a..b]toReadOnlyMemory<char>, but there doesn't seem to be an implicit conversion there.Does not detect
Explicit conversions:
ReadOnlySpan<char> slice = (ReadOnlySpan<char>)str[a..b];ReadOnlySpan<SomeT> slice = (ReadOnlySpan<SomeT>)arr[a..b];ReadOnlySpan<SomeT> slice = (Span<SomeT>)arr[a..b];ReadOnlyMemory<SomeT> slice = (ReadOnlyMemory<SomeT>)arr[a..b];ReadOnlyMemory<SomeT> slice = (Memory<SomeT>)arr[a..b];Span<SomeT> slice = (Span<SomeT>)arr[a..b];Memory<SomeT> slice = (Memory<SomeT>)arr[a..b];Fixes
ReadOnlySpan<char> slice = str.AsSpan()[a..b];ReadOnlySpan<SomeT> slice = arr.AsSpan()[a..b];ReadOnlyMemory<SomeT> slice = arr.AsMemory()[a..b];Span<SomeT> slice = arr.AsSpan()[a..b];Memory<SomeT> slice = arr.AsMemory()[a..b];Fixes dotnet/runtime#33829
Because the string type is immutable,
ReadOnlySpan<char>from a string is also immutable, therefore that diagnostic has a very low chance of being a false positive.For arrays it's possible (but unlikely) that the caller wants the copy to avoid side effects, such as GC lifetime or P/Invokes disagreeing on how ReadOnly the Span was. Because of that, the array to ReadOnly{Span|Memory} and writable {Span|Memory} are different codes from the string one; and since the read-only version feels like it's almost never wrong, it's yet a third code.