A bunch of analysers I find useful - including a defensive copies analyser.
See Shipped Analysers on GitHub for the list of analysers included.
- Configuration
- Defensive Copies Analysers (C# Only)
- Non-Ordinal String APIs Analyser
- Nullable If Analyser (VB.NET Only)
You can use the MSBuild property Hamarb123AnalyzersDiagnosticsIncludeList to specify an include-list of analysers to run if that's your preference.
For example:
<Hamarb123AnalyzersDiagnosticsIncludeList>HAM0001;HAM0003</Hamarb123AnalyzersDiagnosticsIncludeList>A defensive copy is something the C# compiler emits in some scenarios to ensure that readonly memory is not mutated.
For example:
class Class1
{
//A defensive copy is emitted for field1 since we call a mutating method on readonly memory:
public static int GetValue() => field1.Increment();
private static readonly Struct1 field1;
}
struct Struct1
{
private int i;
public int Increment() => i++;
}Analysers are included to catch where these occur:
HAM0001(warning) where it's meaningfully different behaviour.HAM0003(info) where it's known to be only unnecessary.
Idea behind the below precise definition:
- I consider a defensive copy to basically be: whenever a copy is made as a result of memory not being mutable, that results in a meaningful behavioural difference (compared to if the memory was mutable).
- For classes, this is never the case, since
thisis not passed by-reference for them (therefore, whether a copy was made ofthisbefore calling is irrelevant). - When calling members on structs cause a defensive copy to be made, there is a meaningful difference since
thisis passed by-reference, and therefore a differentthisis received. - In practical terms though, members that are known to have
readonlyimplementations onstructs only cause an IL size and/or performance difference (hence why these areHAM0003(info) instead ofHAM0001(warning)).
Precise definition of defensive copy (HAM0001):
- A copy of some readonly memory (LHS) is made, so a potentially mutating member (RHS) (which will be a method in IL) can be called.
- This requires that wrapping
Unsafe.AsRefaround LHS would elide the copy (otherwise, it's not a defensive copy, just a copy), ignoring safety and issue ofref structs in generics for a moment.
- This requires that wrapping
- If call is on a guaranteed non-
valuetype, then it's never a defensive copy (since call to method on defensive copy ofclassis not observable). - If member implementation is known to be actually
readonly(includesconstrainedimplementations on knownstructtypes that don't override it), then it is instead aHAM0003defensive copy. - "Actually
readonlymember" definition:- Based on metadata information only, no source info or special knowledge (other than what is guaranteed by runtime) is allowed.
- Not marked
readonly, or otherwise causes a defensive copy. - It must be possible to safely elide the defensive copy with
Unsafe.AsRef(ignoring issue ofref structs in generics), when assuming that the memory location it's stored in is trulyreadonly.
Some APIs on string do not use ordinal string comparison - this analyser (HAM0004) warns on those.
For example:
class Class1
{
public bool M(string str)
{
//The following returns true for the empty string in latest .NET versions, even though it doesn't contain a null character - this API does not use ordinal string comparison by default.
return str.EndsWith("\0");
}
}In VB.NET, Boolean? can be used directly in If, ternary, etc. - this often leads to unexpected behaviour for those familiar with C# - this analyser (HAM0002) warns on those.
For example:
Class Class1
Public Sub M1(str As String)
If str?.GetHashCode() <> 0 Then
'This branch is not taken when str is Nothing, even though `str?.GetHashCode()` looks like it should give `Nothing` which is indeed not `0`
End If
End Sub
End Class