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

[WIP] Add rule for detecting certain readonly mutable structs #2831

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

mareklinka
Copy link

@mareklinka mareklinka commented Sep 8, 2019

As proposed in corefx/issues/40739 and #2811, this PR adds a new rule for detecting readonly fields of type System.Threading.SpinLock and System.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


namespace Microsoft.NetCore.Analyzers.Performance
{
public abstract class MutableStructsShouldNotBeUsedForReadonlyFieldsAnalyzer : DiagnosticAnalyzer
Copy link
Contributor

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();
Copy link
Contributor

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:

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)
Copy link
Contributor

@mavasani mavasani Sep 8, 2019

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:

  1. Analyze the attributes/properties of a symbols: Prefer RegisterSymbolAction
  2. 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.

Copy link
Member

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.

Copy link
Member

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(
Copy link
Contributor

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)

Copy link
Author

@mareklinka mareklinka Sep 8, 2019

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:

  1. 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.
  2. That means the fix will get a span that points to a VariableDeclaratorSyntax, not the actual FieldDeclarationSyntax. That works fine for the SyntaxGenerator, which ascends the tree correctly and returns a new FieldDeclarationSyntax node, but then I can't simply call root.ReplaceNode because the nodes are of different types.
  3. 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 new FieldDeclarationSyntax 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.

Copy link
Contributor

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>
Copy link
Contributor

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

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();
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 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()
Copy link
Contributor

@mavasani mavasani Sep 8, 2019

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);
Copy link
Contributor

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()
Copy link
Contributor

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>
Copy link
Contributor

@mavasani mavasani Sep 8, 2019

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>
Copy link
Contributor

@mavasani mavasani Sep 8, 2019

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:

  1. Use the existing option disallowed_symbol_names
  2. 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.

Copy link
Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds reasonable!

Copy link
Member

@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.

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.

@Evangelink
Copy link
Member

@sharwell, @mavasani have you been able to meet and discuss about the direction this PR should go to?

@Evangelink
Copy link
Member

@mareklinka Is there something I can do to help moving forward with this new analyzer?

@mareklinka
Copy link
Author

@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.

@Evangelink
Copy link
Member

@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.

@sharwell
Copy link
Member

@mareklinka I implemented the initial non-copyable analysis in #3420

Base automatically changed from master to main March 5, 2021 02:20
@jinujoseph jinujoseph requested a review from a team as a code owner March 5, 2021 02:20
@sharwell
Copy link
Member

@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.

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

Successfully merging this pull request may close these issues.

New rule: using mutable structs in readonly fields
4 participants