Set of analyzers for a better engineering experience.
Code Quality Analyzer: Require using statement/declaration modifier for classes (SE1000, SE1001, SE1002)
This analyzer checks if a class is decorated with the RequireUsingAttribute
attribute and if it is it verifies that the class is instantiated within a using statement or using declaration.
In cases where the caller knows that they are doing something safe, they can suppress the waring by calling the ExcludeFromUsingAsync
or ExcludeFromUsing
methods.`
[RequireUsing]
public class DisposableClass : IDisposable
{
public void Dispose()
{
// Dispose implementation
}
}
public class Caller
{
public void Call()
{
// This will raise a warning
var disposable = new DisposableClass();
// This will not raise a warning
using (var disposable = new DisposableClass())
{
// Do something with disposable
}
// This will not raise a warning
var disposable = new DisposableClass().ExcludeFromUsing();
}
}
- 2024-10-04: Added support for duck-typed ref struct usage (when a ref struct has a public
Dispose
orDisposeAsync
methods)
What it looks for: This analyzer checks if a type or method is being instantiated or called outside a using statement or using declaration. This also includes types or methods with the RequireUsingAttribute
attribute.
Why: In C#, a using statement ensures that the Dispose method is called on an object when it is no longer needed, crucial for RIAA scenarios. For types or methods that need to be disposed of properly, it is important to use them within a using statement or declaration.
How it helps engineering: It helps in enforcing best practices in resource management, thus preventing potential memory leaks or RIAA scenarios which can negatively impact the application performance.
What it looks for: This analyzer checks if a class with the RequireUsingAttribute attribute implements the IDisposable or IAsyncDisposable interfaces.
Why: The RequireUsingAttribute is intended for types that need to be disposed of properly, and these types should implement the IDisposable or IAsyncDisposable interfaces.
How it helps engineering: It ensures that only types that can be properly disposed of are marked with the RequireUsingAttribute, helping to prevent improper resource management.
What it looks for: This analyzer checks if a method with the RequireUsingAttribute attribute returns a type that implements the IDisposable or IAsyncDisposable interfaces.
Why: The RequireUsingAttribute is intended for methods that return types that need to be disposed of properly, and these returned types should implement the IDisposable or IAsyncDisposable interfaces.
How it helps engineering: It ensures that methods marked with the RequireUsingAttribute return types that can be properly disposed of, helping to enforce best practices in resource management.
This analyzer checks for using statements that import relative namespaces.
namespace MyNamesace.MySubNamespace1
{
// ...
}
namespace MyNamesace.MySubNamespace2
{
using MySubNamespace1; // This will raise a warning
}
What it looks for: This analyzer checks for using statements that import relative namespaces.
Why: Using relative namespaces in using directives can lead to confusion and can be prone to errors as the codebase evolves or copied around. In some cases, with classes that have popular names, this may actually silently add bugs.
How it helps engineering: By enforcing the use of absolute namespace paths in using directives, it helps to ensure that the correct namespaces are being used, which can increase the readability and maintainability of the codebase.
A code fix is available for SE1010 - changing the relative namespace into an absolute one.
Applies to types decorated with the ExhaustiveInitializationAttribute
attribute.
This analyzer verifies that all properties in the type are initialized in the constructor or have the required
modifier.
[ExhaustiveInitialization]
public class MyClass
{
public int MyProperty { get; set; } // This will raise a warning because it's not initialized in the constructor or marked as required
}
What it looks for: This analyzer checks for types that are not designed to be exhaustively initialized.
Why: In certain cases, it's advantageous to ensure that all properties of a type are initialized in the constructor or marked as required. This can help prevent omission bugs and make the code more robust. A good example for this are types that are used as DTOs or models where they need to be converted to and from - marking both class as ExhaustiveInitialization can help ensure that all properties are properly initialized.
How it helps engineering: By enforcing exhaustive initialization, it helps to ensure that all properties of a type are properly initialized, which can help prevent bugs and make the code more robust.
What it looks for: This is a warning on specific properties that are not required, or otherwise initialized in the constructor.
Why: See SE1030
How it helps engineering: See SE1030
What it looks for: When a class (not record) or a struct (not a record struct) contains multiple constructors, and not all properties are required, the analyzer will raise a warning that it does not know how to handle it.
Why: Reduce chances of mis-creating the class.
How it helps engineering: See SE1030
What it looks for: Primary constructors are not allowed on classes and structs that are not records.
Why: There's no good way to inspect what the primary constructor is doing, and so exhaustiveness cannot be enforced.
How it helps engineering: See SE1030
What it looks for: Primary constructors are not allowed to have default values on implicit constructor properties.
Why: Default arguments on the primary constructor are not allowed on exhauste initialization types.
How it helps engineering: See SE1030
A code fix is available for SE1030 and SE1031 - will add required to properties that are missing it.
When obsoleting members, it's often a good idea to hide it from intellisense so that developers have less of a chance of using it accidentlly.
SE1040: ObsoleteELementsShouldBeHidden
What it looks for: When an entity is marked with the [Obsolete] attribute, but not with the [EditorBrowsable(EditorBrowsableState.Never)] attribute
Why: When obsoleting an entity, it's preferable that developers stop seeing it in their intellisense. Note that if it needs to be visible, the developer may also decide to use other values of EditorBrowsableState.
How it helps engineering: Less chance of deprecated elements being used in new code.
When a developer wants to make sure they always check all possible values of an enum, then can mark the enum value as exhaustive - the analyzer will then make sure all known values are checked.
Note: The ForceExhaustive
method that is used for this, does nothing but return the enum value to the caller. It's just used to mark the requirement for the analyzer.
public enum MyState
{
Default,
Created = Default,
Started,
Running,
Completed,
Abandoned // Newly added enum value
}
public string? MetricName(MyState state)
{
return state.ForceExhaustive() switch // This will give an SE1050 warning/error because MyState.Abandoned is missing here.
{
MyState.Created => "Created", // Created covers Default, so it does not need to be mentioned in the switch
MyState.Started => null,
MyState.Running => "Active",
MyState.Completed => "Done",
_ => throw InvalidOperationException($"Unrecognized enum value {state}")
}
}
What it looks for: When a switch statement or expression is applied to a value of an enum, and that enum has been passed to the Extension method ForceExhaustive
, the analyzer will go through the switch and make sure all possible enum values are inspected.
Why: In some cases, a developer knows that all values of an enum need to be considered in certaina areas of the code. In such cases, the developer can mark the code with ForceExhaustive
and if anyone adds an enum value later on, the code the developer wrote will get the warning/error.
How it helps engineering: It' harder to make mistakes as code evolves.
This is to make sure developers don't accidetnally use this method in other places.
The only patterns supported right now by an exhaustive switch statement are normal matches and or
matches:
var x = e.ForceExhaustive() switch
{
MyEnum.Value1 => // Allowed
MyEnum.Value1 or MyEnum.Value2 => // Allowed
// No other pattern is allowed:
MyEnum why => // Disallowed
MyEnum.Value1 and MyEnum.Value2 => // Disallowed and doesnt make sense.
etc...
}
For performance-sensitive code, it's sometimes useful for the developer to prevent misuse of a method or class to protect the developer.
Using the [UseStaticLambda] on a field, property or parameter will cause the analyzer to emit a warning if a non-static lambda is passed in.
public ref struct MyOptimizedNonAllocUtility
{
public MyOptimizedNonAllocUtility([UseStaticLambda] Func<int> myDelegate)
{
_myDelegate = myDelegate;
}
}
// Code using the struct
var s = new MyOptimizedNonAllocUtility(() => 42); // This will emit SE1060
// Fixed code:
var s = new MyOptimizedNonAllocUtility(static () => 42); // This will emit SE1060
What it looks for: Passing in non-static delegates to methods that expect to get a static.
Why: For performance-sensitive code that uses delegates, it's easy to sometimes forget that creating a capturing delegate can be expensive. This will remind the developer to use a delegate that will not allocate or allocate less.
How it helps engineers: Reduces the chance of making such mistakes.