-
Notifications
You must be signed in to change notification settings - Fork 468
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
[WIP] Add rule for detecting certain readonly mutable structs #2831
base: main
Are you sure you want to change the base?
[WIP] Add rule for detecting certain readonly mutable structs #2831
Conversation
|
||
namespace Microsoft.NetCore.Analyzers.Performance | ||
{ | ||
public abstract class MutableStructsShouldNotBeUsedForReadonlyFieldsAnalyzer : DiagnosticAnalyzer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This analyzer can be written as a language agnostic symbol analyzer, which just registers a SymbolAction with SymbolKind.Field
. For example, look at https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeQuality.Analyzers/Core/ApiDesignGuidelines/DoNotDeclareVisibleInstanceFields.cs.
return; | ||
} | ||
|
||
var typesToCheck = MutableValueTypesOfInterest.Select(typeName => context.Compilation.GetTypeByMetadataName(typeName)).Where(type => type != null).ToList(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GetTypeByMetadataName
should only be invoked during CompilationStartAction callback. You should check for null values of the required well known type(s) and bail out if the analyzer is not applicable for the current compilation, and register nested symbol action if it is applicable. For example:
Lines 42 to 54 in 3e4ef01
context.RegisterCompilationStartAction(compilationContext => | |
{ | |
// Check if TPL is available before actually doing the searches | |
var taskType = compilationContext.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task"); | |
var taskFactoryType = compilationContext.Compilation.GetTypeByMetadataName("System.Threading.Tasks.TaskFactory"); | |
var taskSchedulerType = compilationContext.Compilation.GetTypeByMetadataName("System.Threading.Tasks.TaskScheduler"); | |
if (taskType == null || taskFactoryType == null || taskSchedulerType == null) | |
{ | |
return; | |
} | |
compilationContext.RegisterOperationAction(operationContext => | |
{ |
context.RegisterSyntaxNodeAction(NodeAction, SyntaxKind.FieldDeclaration); | ||
} | ||
|
||
private static void NodeAction(SyntaxNodeAnalysisContext context) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, we strongly recommend not using SyntaxNodeAction in this repo. Majority of analyzers should fall under the below 2 buckets:
- Analyze the attributes/properties of a symbols: Prefer RegisterSymbolAction
- Analyze executable code: Prefer RegisterOperationAction
SyntaxNodeActions are only required for analyses related to syntax nodes that are not part of any symbol's declaration OR executable code, which is extremely rare for an analyzers in this repo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 Note that syntax node actions are not inherently bad. However, the GetSymbolInfo
method is very expensive, and the cost can be avoided by using one of the other approaches. If you need to operate on symbols (as opposed to just syntax), symbol and operation actions are more efficient. In some cases, operation actions also simplify the work required to support both C# and VB.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mareklinka regarding the "rewrite" of this analyzer, you can have a look at this PR I started: https://github.com/dotnet/roslyn-analyzers/pull/3405/files
FieldDeclarationSyntax fieldDeclarationSyntax) | ||
{ | ||
var editor = await DocumentEditor.CreateAsync(context.Document, context.CancellationToken).ConfigureAwait(false); | ||
var withoutReadonly = fieldDeclarationSyntax.WithModifiers( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A slighly simpler implementation would be: http://source.roslyn.io/#Microsoft.CodeAnalysis.CSharp.Features/CodeFixes/HideBase/HideBaseCodeFixProvider.AddNewKeywordAction.cs,37
This also allows you to make the fixer language agnostic (you don't need language specific SyntaxKind
anymore)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried approaching it as you suggested but I think I'm missing something. The situation:
- The analyzer now operates on symbols and reports the location of the diagnostic as
fieldSymbol.Locations.First()
. This means the location of the symbol name is reported. - That means the fix will get a span that points to a
VariableDeclaratorSyntax
, not the actualFieldDeclarationSyntax
. That works fine for theSyntaxGenerator
, which ascends the tree correctly and returns a newFieldDeclarationSyntax
node, but then I can't simply callroot.ReplaceNode
because the nodes are of different types. - Furthermore, this complicates executing the fix on statement
private readonly SpinLock _sl1 = new SpinLock(), sl2 = new SpinLock();
. Here the generator doesn't work because it creates a newFieldDeclarationSyntax
but without the second variable declaration.
Either I'm missing the correct API/usage of it or it looks like it will be easier to make the fix language-specific and simply use:
root.FindNode(context.Span).FirstAncestorOrSelf<FieldDeclarationSyntax>() // this is language-specific
...
var generator = SyntaxGenerator.GetGenerator(context.Document);
var nodeWithoutReadonly = generator.WithModifiers(targetNode, generator.GetModifiers(targetNode).WithIsReadOnly(false));
var newRoot = root.ReplaceNode(originalFieldDeclaration, nodeWithoutReadonly);
return Task.FromResult(context.Document.WithSyntaxRoot(newRoot));
Testing this, it leads to correct behavior even in case of multi-declaration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I meant to recommend the exact same implementation as your comment, but keep it in the shared layer with a type parameter TFieldDeclarationSyntax, where TFieldDeclarationSyntax: SyntaxNode. Language specific subtypes will just need to provide the language specific type argument FieldDeclarationSyntax and have no code within.
private GCHandle _gch_noinit; | ||
} | ||
"; | ||
await CSharpCodeFixVerifier<CSharpMutableStructsShouldNotBeUsedForReadonlyFieldsAnalyzer, CSharpMutableStructsShouldNotBeUsedForReadonlyFieldsFixer> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kindly follow the pattern used by all the existing unit tests in the repo and define usings to avoid clutter in individial unit tests such as
roslyn-analyzers/src/Microsoft.NetCore.Analyzers/UnitTests/Runtime/TestForNaNCorrectlyTests.Fixer.cs
Lines 8 to 52 in c4e5473
using VerifyCS = Test.Utilities.CSharpSecurityCodeFixVerifier< | |
Microsoft.NetCore.Analyzers.Runtime.TestForNaNCorrectlyAnalyzer, | |
Microsoft.NetCore.CSharp.Analyzers.Runtime.CSharpTestForNaNCorrectlyFixer>; | |
using VerifyVB = Test.Utilities.VisualBasicSecurityCodeFixVerifier< | |
Microsoft.NetCore.Analyzers.Runtime.TestForNaNCorrectlyAnalyzer, | |
Microsoft.NetCore.VisualBasic.Analyzers.Runtime.BasicTestForNaNCorrectlyFixer>; | |
namespace Microsoft.NetCore.Analyzers.Runtime.UnitTests | |
{ | |
public class TestForNaNCorrectlyFixerTests | |
{ | |
[Fact] | |
public async Task CA2242_FixFloatForEqualityWithFloatNaN() | |
{ | |
await VerifyCS.VerifyCodeFixAsync(@" | |
public class A | |
{ | |
public bool Compare(float f) | |
{ | |
return [|f == float.NaN|]; | |
} | |
} | |
", @" | |
public class A | |
{ | |
public bool Compare(float f) | |
{ | |
return float.IsNaN(f); | |
} | |
} | |
"); | |
await VerifyVB.VerifyCodeFixAsync(@" | |
Public Class A | |
Public Function Compare(s As Single) As Boolean | |
Return [|s = Single.NaN|] | |
End Function | |
End Class | |
", @" | |
Public Class A | |
Public Function Compare(s As Single) As Boolean | |
Return Single.IsNaN(s) | |
End Function | |
End Class | |
"); |
|
||
public class C | ||
{ | ||
private [|readonly|] SpinLock _sl = new SpinLock(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add C# and VB unit tests where you have a field declaration with multiple fields declared, such as private readonly SpinLock _sl1 = new SpinLock(), sl2 = new SpinLock();
Imports System.Runtime.InteropServices | ||
|
||
Public Class Class1 | ||
Public [|ReadOnly|] _sl As SpinLock = New SpinLock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add VB unit test with field declaration that using AsNew clause? For example, Private Readonly _sl As New SpinLock()
|
||
protected abstract void AnalyzeCodeFix(CodeFixContext context, SyntaxNode targetNode); | ||
|
||
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(MutableStructsShouldNotBeUsedForReadonlyFieldsAnalyzer.RuleId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Move the property overrides before the method overrides.
|
||
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(MutableStructsShouldNotBeUsedForReadonlyFieldsAnalyzer.RuleId); | ||
|
||
public override FixAllProvider GetFixAllProvider() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Move this override ahead of RegisterCodeFixesAsync
override.
<value>Certain value types (e.g. SpinLock, GCHandle) are explicitly designed to be mutable. Declaring a field of such type as readonly might cause unintentional shadow copying by the compiler, leading to performance degradation or bugs.</value> | ||
</data> | ||
<data name="MutableStructsShouldNotBeUsedForReadonlyFieldsMessage" xml:space="preserve"> | ||
<value>Field {0} of type {1} should not be readonly.</value> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Field '{0}' of type '{1}' should not be readonly.
<value>Certain readonly structs should not be used as readonly fields</value> | ||
</data> | ||
<data name="MutableStructsShouldNotBeUsedForReadonlyFieldsDescription" xml:space="preserve"> | ||
<value>Certain value types (e.g. SpinLock, GCHandle) are explicitly designed to be mutable. Declaring a field of such type as readonly might cause unintentional shadow copying by the compiler, leading to performance degradation or bugs.</value> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that SpinLock
and GCHandle
are just couple of potentially many such types for which this rule can be applicable, I think we should make this configurable for end users to allow specifying the set of disallowed types such that mutable readonly fields of all of those types would be flagged. If we agree, then I would suggest the following:
- Use the existing option
disallowed_symbol_names
- See how this option is used to make CA1031 configurable with this commit: 40cd301. I would suggest a similar implementation and unit test for it.
If you feel this would overload your PR, I can do this as a followup PR as well. In this case, please file a separate tracking issue to make this rule configurable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that configuring this could be useful but I'd opt into enabling this configuration in a separate PR. I'll file it once this one is merged, if that's ok.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds reasonable!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Marking as Request Changes to give me a chance to meet with @mavasani and discuss the distinction between non-readonly and non-copyable. The latter is a more complicated rule, but much better at detecting potential bugs arising from misuse of mutable value types.
@mareklinka Is there something I can do to help moving forward with this new analyzer? |
@Evangelink Well, as this was originally waiting for a clarification meeting and there was no update for a long time, I stopped really paying attention after a while. As @sharwell mentioned, if the analyzer is supposed to be detecting copying of structs, it will be much more complicated. In that case, I think I will leave the implementation to someone with more time for it. If the readonly semantics are fine, I think I should be able to revisit the PR and solve the merge conflicts so that it could be reviewed and merged. |
@mavasani, @sharwell have you been able to discuss #2831 (review)? The original ticket is talking about non-readonly IMO, the non-copyable part would also make sense but could be either an update to this rule or a separate rule. |
@mareklinka I implemented the initial non-copyable analysis in #3420 |
@mavasani My recommendation for this is to use it as more evidence in support of copy value callbacks integrated directly into the analyzer driver as part of preparing for dotnet/runtime#50389. |
As proposed in corefx/issues/40739 and #2811, this PR adds a new rule for detecting readonly fields of type
System.Threading.SpinLock
andSystem.Runtime.InteropServices.GCHandle
.Both C# and VB versions are implemented but VB is not my strong suite so it might not be entirely idiomatic VB.NET.
Tests for both analyzers and fixers are there but if I managed to omit some cases, let me know and I'll add them.
I'm also not sure I'm defensive enough (it's been a while since I worked with Roslyn so I might have missed some recommended practices).
ID of the new rule is Performance/CA1829.
Fixes #2811