-
Notifications
You must be signed in to change notification settings - Fork 229
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
New Rule S2445: Blocks should be synchronized on read-only fields #6750
New Rule S2445: Blocks should be synchronized on read-only fields #6750
Conversation
322e626
to
0479b43
Compare
0479b43
to
94d1887
Compare
@@ -0,0 +1,17 @@ | |||
{ | |||
"issues": [ | |||
{ |
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.
By looking at this test case, I have some doubts about the validity of the rule on local method variables.
The code for this IT is the following:
TypeMap ResolveTypeMap(in TypePair typePair)
{
if (_resolvedMaps.TryGetValue(typePair, out TypeMap typeMap))
{
return typeMap;
}
if (_sealed)
{
typeMap = _runtimeMaps.GetOrAdd(typePair);
// if it's a dynamically created type map, we need to seal it outside GetTypeMap to handle recursion
if (typeMap != null && typeMap.MapExpression == null)
{
lock (typeMap)
{
typeMap.Seal(this, null);
}
}
}
else
{
typeMap = GetTypeMap(typePair);
_resolvedMaps.Add(typePair, typeMap);
}
return typeMap;
}
While typeMap
is a local variable, which can mutate, it is read from _runtimeMaps.GetOrAdd(typePair)
, which is a private readonly LockingConcurrentDictionary<TypePair, TypeMap>
.
So basically the variable itself is not immutable, but the collection from which is read it is, and such a collection is used to have fine-graned locking based on TypePair
.
While this all seems perfectly legitimate to me, and even necessary when you need a variable number of locks, I don't see how to easily distinguish between this scenario and the other ones.
Do we accept it as a false positive, or do we reduce the scope of the rule, to just ignore local variables?
What do you think?
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 think it can be refined by adding an exception to the rule: if the code block is locked on a local variable and that local variable is assigned the value of a method call then it should be ignored.
var localVar = MethodCall();
...
lock (localVar) // Compliant
{
...
}
But I'd do this in a separate PR, and mark it as FP for now.
@andrei-epure-sonarsource What do you think?
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.
sorry I didn't reply. If it's an edge case, it's acceptable to have an FP.
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.
While it appears in our ITs, it seems to me enough rare, to consider it as an FP.
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.
Kudos for the level of detail in your test cases!
I only have some minor comments.
The additional test cases I mention in my comments are optional. I'll leave it to you to decide whether to add them or not.
@@ -0,0 +1,17 @@ | |||
{ | |||
"issues": [ | |||
{ |
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 think it can be refined by adding an exception to the rule: if the code block is locked on a local variable and that local variable is assigned the value of a method call then it should be ignored.
var localVar = MethodCall();
...
lock (localVar) // Compliant
{
...
}
But I'd do this in a separate PR, and mark it as FP for now.
@andrei-epure-sonarsource What do you think?
analyzers/tests/SonarAnalyzer.UnitTest/TestCases/LockedFieldPrivateReadonly.cs
Outdated
Show resolved
Hide resolved
analyzers/tests/SonarAnalyzer.UnitTest/TestCases/LockedFieldPrivateReadonly.cs
Outdated
Show resolved
Hide resolved
analyzers/tests/SonarAnalyzer.UnitTest/TestCases/LockedFieldPrivateReadonly.cs
Outdated
Show resolved
Hide resolved
analyzers/tests/SonarAnalyzer.UnitTest/TestCases/LockedFieldPrivateReadonly.cs
Outdated
Show resolved
Hide resolved
analyzers/tests/SonarAnalyzer.UnitTest/TestCases/LockedFieldPrivateReadonly.cs
Outdated
Show resolved
Hide resolved
analyzers/tests/SonarAnalyzer.UnitTest/TestCases/LockedFieldPrivateReadonly.cs
Outdated
Show resolved
Hide resolved
analyzers/tests/SonarAnalyzer.UnitTest/TestCases/LockedFieldPrivateReadonly.cs
Outdated
Show resolved
Hide resolved
analyzers/tests/SonarAnalyzer.UnitTest/TestCases/LockedFieldPrivateReadonly.cs
Outdated
Show resolved
Hide resolved
analyzers/tests/SonarAnalyzer.UnitTest/TestCases/LockedFieldPrivateReadonly.CSharp8.cs
Outdated
Show resolved
Hide resolved
f9dbfdd
to
5be33e4
Compare
Intentionally avoided |
bc4f07f
to
9923a89
Compare
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.
LGTM
I only added suggestions for some minor fixes.
analyzers/src/SonarAnalyzer.CSharp/Rules/LockedFieldShouldBePrivateAndReadonly.cs
Outdated
Show resolved
Hide resolved
analyzers/src/SonarAnalyzer.CSharp/Rules/LockedFieldShouldBePrivateAndReadonly.cs
Outdated
Show resolved
Hide resolved
analyzers/tests/SonarAnalyzer.UnitTest/TestCases/LockedFieldShouldBePrivateAndReadonly.cs
Outdated
Show resolved
Hide resolved
77087ca
to
9089b70
Compare
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.
Suggestions to make things a bit clearer.
analyzers/src/SonarAnalyzer.CSharp/Rules/LockedFieldShouldBePrivateAndReadonly.cs
Outdated
Show resolved
Hide resolved
analyzers/src/SonarAnalyzer.CSharp/Rules/LockedFieldShouldBePrivateAndReadonly.cs
Outdated
Show resolved
Hide resolved
analyzers/src/SonarAnalyzer.CSharp/Rules/LockedFieldShouldBePrivateAndReadonly.cs
Outdated
Show resolved
Hide resolved
analyzers/src/SonarAnalyzer.CSharp/Rules/LockedFieldShouldBePrivateAndReadonly.cs
Outdated
Show resolved
Hide resolved
analyzers/src/SonarAnalyzer.CSharp/Rules/LockedFieldShouldBePrivateAndReadonly.cs
Outdated
Show resolved
Hide resolved
&& symbol is IFieldSymbol lockedField | ||
&& (!lockedField.IsReadOnly || lockedField.GetEffectiveAccessibility() != Accessibility.Private)) | ||
{ | ||
if (lockedField.ContainingType is { } lockedFieldType && context.ContainingSymbol?.ContainingType is { } containingType && !lockedFieldType.Equals(containingType)) |
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.
also this condition would be nice to be extracted to a local function with a name that describes what it checks.
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.
Done.
As in #6750 (comment), that has decreased the method complexity at the expense of the overall complexity of the class, due to an indentation more and the use of ?.
.
...zers/tests/SonarAnalyzer.UnitTest/TestCases/LockedFieldShouldBePrivateAndReadonly.CSharp9.cs
Outdated
Show resolved
Hide resolved
analyzers/tests/SonarAnalyzer.UnitTest/TestCases/LockedFieldShouldBePrivateAndReadonly.cs
Outdated
Show resolved
Hide resolved
analyzers/tests/SonarAnalyzer.UnitTest/TestCases/LockedFieldShouldBePrivateAndReadonly.cs
Outdated
Show resolved
Hide resolved
analyzers/tests/SonarAnalyzer.UnitTest/TestCases/LockedFieldShouldBePrivateAndReadonly.cs
Outdated
Show resolved
Hide resolved
49b62bb
to
22b02cc
Compare
@mary-georgiou-sonarsource Assigned back to you despite the failing checks on |
@antonioaversa I think the issue was fixed on master so if you rebase it should be ok in this PR as well. |
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.
Nitpicks
|
||
private static bool IsOfTypeString(SemanticModel model, ExpressionSyntax expression) => | ||
expression.IsAnyKind(SyntaxKind.StringLiteralExpression, SyntaxKind.InterpolatedStringExpression) | ||
|| expression.IsKnownType(KnownType.System_String, model); |
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.
|| expression.IsKnownType(KnownType.System_String, model); | |
|| expression.IsKnownType(KnownType.System_String, model); |
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 have made the change.
I am a bit lost with the indentation rules here: they seem context-sensitive.
For example:
{
return A
&& B
}
but
=>
A
&& B
So, basically &&
indents to A, or to its level above, depending on the context.
Coding styles, as defined here, don't seem to define a rule precisely.
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.
@antonioaversa Indeed - could you please drop a message for this in the team channel on slack?
analyzers/src/SonarAnalyzer.CSharp/Rules/LockedFieldShouldBePrivateAndReadonly.cs
Outdated
Show resolved
Hide resolved
analyzers/tests/SonarAnalyzer.UnitTest/TestCases/LockedFieldShouldBePrivateAndReadonly.cs
Outdated
Show resolved
Hide resolved
...zers/tests/SonarAnalyzer.UnitTest/TestCases/LockedFieldShouldBePrivateAndReadonly.CSharp9.cs
Outdated
Show resolved
Hide resolved
analyzers/src/SonarAnalyzer.CSharp/Rules/LockedFieldShouldBePrivateAndReadonly.cs
Outdated
Show resolved
Hide resolved
analyzers/src/SonarAnalyzer.CSharp/Rules/LockedFieldShouldBePrivateAndReadonly.cs
Outdated
Show resolved
Hide resolved
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 left a comment , but don't wait for my approval
22b02cc
to
f27da3b
Compare
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.
Offline discussion with @antonioaversa and @pavel-mikula-sonarsource
The scope of the rule will change a bit so that it covers scenarios that won't raise too many FPS.
We ask the lock field to be readonly + not a string + not a new instance of an object.
…ementation and names
2cb6366
to
829c892
Compare
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.
Nitpicks.
expression.IsAnyKind(SyntaxKind.StringLiteralExpression, SyntaxKind.InterpolatedStringExpression) | ||
|| lazySymbol.Value.GetSymbolType().Is(KnownType.System_String); | ||
|
||
private static IFieldSymbol FieldWritable(ExpressionSyntax expression, Lazy<ISymbol> lazySymbol) => |
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.
private static IFieldSymbol FieldWritable(ExpressionSyntax expression, Lazy<ISymbol> lazySymbol) => | |
private static IFieldSymbol FieldIsWritable(ExpressionSyntax expression, Lazy<ISymbol> lazySymbol) => |
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.
The name FieldIsWritable
seems to assume that the expression
provided as input is necessarily a field.
In this case the method receives an ExpressionSyntax
which may or may not be a field: for example it may be a SimpleMemberAccessExpressionSyntax
accessing a property, for which Expression
would be an IPropertySymbol
.
IsFieldWritable
would already be a better name, but imo violates the principle of least astonishment: one would expect a method starting with Is
to return a bool
. Instead, this method returns a IFieldSymbol
which may or may not be null, depending on whether all the conditions of "being a member", "referring to a field" and "being a non-readonly field" are respected.
The closest semantics to this is the one of (a very elaborate) as
operator: if you prefer, I may change the name into AsWritableField
. WritableField
may suggest a "cast" semantic, i.e. it may suggest throwing an exception rather than returning null
.
However, I would avoid IsFieldWritable
or FieldIsWritable
... unless there is a specific code convention driving the choice. If there's not, I would assume any options of the ones above, except maybe IsFieldWritable
due to POLA violation, should be considered valid for this PR.
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.
🤦 my bad - you are right.
I think I'd just change it then to FindWritableField
or ExtractFieldSymbol
or something a bit more descriptive if you have other ideas.
IMO just FieldWritable
is a bit confusing.
static readonly object staticReadonlyField = null; | ||
static object staticReadWriteField = null; | ||
|
||
readonly object readonlyField = null; | ||
object readWriteField = null; |
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.
These seem to be unused fields - in case they should be deleted to reduce the noise.
class Test | ||
{ | ||
static readonly object staticReadonlyField = null; | ||
static object staticReadWriteField = null; |
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.
Same for this - it can be deleted.
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.
3 out of 4 should be removed
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.
staticReadWriteField
seems the only one which is not used anymore:
staticReadonlyField
reference is returned on line 29readonlyField
reference is returned on line 14readWriteField
reference is returned on line 17
@pavel-mikula-sonarsource: was it rather 3 out of 4 to be kept?
analyzers/tests/SonarAnalyzer.UnitTest/TestCases/LockedFieldShouldBeReadonly.cs
Show resolved
Hide resolved
} | ||
} | ||
|
||
class AnotherClass |
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.
Maybe change the name of this class to reflect better what it's used to test.
I had to do couple of ups and downs to really understand.
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.
LGTM with some comments to be addressed before merging
class Test | ||
{ | ||
static readonly object staticReadonlyField = null; | ||
static object staticReadWriteField = null; |
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.
None of the fields here is used => remove them all
class Test | ||
{ | ||
static readonly object staticReadonlyField = null; | ||
static object staticReadWriteField = null; |
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.
3 out of 4 should be removed
{ | ||
ReportIssue("strings as they can be interned"); | ||
} | ||
else if (expression is IdentifierNameSyntax && lazySymbol.Value is ILocalSymbol lockedSymbol) |
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.
All of them are locked, this branch is distinguished by being local
else if (expression is IdentifierNameSyntax && lazySymbol.Value is ILocalSymbol lockedSymbol) | |
else if (expression is IdentifierNameSyntax && lazySymbol.Value is ILocalSymbol localSymbol) |
@antonioaversa you have the approvals, can we get this closed asap today to do the validation? if there are still some edge-case tests to be added or nitpicks to be addressed, they can be left out , we need to move forward, validate on Peach and close the sprint |
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.
LGTM - There are some small nitpicks only left - no need to re-approve.
@pavel-mikula-sonarsource , @mary-georgiou-sonarsource : I have pushed a commit with the changes done after the last round of review. I am merging this, so that @zsolt-kolbay-sonarsource can deploy the master on peach and we can do validation of the last GitHub issues of the last sprint. |
Kudos, SonarCloud Quality Gate passed! |
Kudos, SonarCloud Quality Gate passed! |
Fixes #6701
Related RSPEC update: SonarSource/rspec#1570
Remarks:
Naming
Description and naming based on the fact that, while in Java blocks are synchronized (the verb) by having fields in
synchronized
(the keyword), in C#/VB.NET blocks are synchronized (they aren't locked themselves) by having fields inlock
(the keyword). C# doesn't have support for synchronized methods.Rule interpretation
The interpretation of the rule is that, when a block is synchronized on a field F then F should be both
private
andreadonly
.Another possible interpretation is the following: when a block is synchronized on F then F should be a field,
private
andreadonly
.The second interpretation would produce many false positives, as it is not uncommon in C# to use private properties wrapping the backing field.
Moreover, methods may be used to return a dynamically calculated object, which should be the target of the lock. That seems reasonable, especially when the locking behavior should be fine-grained (e.g. locking on a row object, a col object, or a cell object, depending on a complex condition).
For these reasons (plus the fact that the rule is of type
BUG
), the first interpretation was chosen.False negatives
Of course, a method could hide the use of a local variable:
lock ((x => localVar)()) { ... }
.Similarly, assignment is not supported and can also hide the use of a local variable:
lock (x = localVar) { ... }
.Since the rule is a
BUG
defined in SonarWay, having all those scenarios as false negative was the selected behavior.