Avoid types initialization cycles
The class constructor (static constructor, cctor in IL) runs the first time a type is used; a static field inline initialization is enough to declare it implicitly.
If the cctor of t1 uses t2 and vice-versa (cycles can span N types), unexpected hard-to-diagnose initialization bugs can occur. More: http://codeblog.jonskeet.uk/2012/04/07/type-initializer-circular-dependencies/
This rule enumerates such cycles. Lambdas defined in cctors (or methods they call) are considered executed at initialization time, which may cause false positives.
If the cctor of t1 uses t2 and vice-versa (cycles can span N types), unexpected hard-to-diagnose initialization bugs can occur. More: http://codeblog.jonskeet.uk/2012/04/07/type-initializer-circular-dependencies/
This rule enumerates such cycles. Lambdas defined in cctors (or methods they call) are considered executed at initialization time, which may cause false positives.
How to fix
If several states held by several classes must be initialized on first access of any of them, create a dedicated class that initializes and holds all these states.
Estimated Debt: 20 minutes per cycle plus 10 minutes per class constructor involved.
Estimated Debt: 20 minutes per cycle plus 10 minutes per class constructor involved.
CQLinq source
This rule is written in CQLinq, C# LINQ over the NDepend code model. It ships with every new NDepend project: you can read it, adapt it, or use it as a starting point for your own custom rules.
// <Name>Avoid types initialization cycles</Name>
// <Id>ND1213:AvoidTypesInitializationCycles</Id>
warnif count > 0
// Types initialization cycle can only happen between types of an assembly.
from assembly in Application.Assemblies
let cctorSuspects = assembly.ChildMethods.Where(
m => m.IsClassConstructor &&
// Optimization: types involved in a type cycle necessarily don't have type level.
m.ParentType.Level == null)
where cctorSuspects.Count() > 1
let typesSuspects = cctorSuspects.ParentTypes().Where(t0 => !t0.Name.Contains("z__a_")).ToHashSetEx()
//
// dicoTmp associates to each type suspect T, a set of types from typesSuspects
// that contains at least a method or a field used directly or indirectly by the cctor of T.
//
let dicoTmp = cctorSuspects.ToDictionary(
cctor => cctor.ParentType,
cctor => ((IMember)cctor).ToEnumerable().FillIterative(
members => from m in members
from mUsed in (m is IMethod) ? (m as IMethod).MembersUsed : new IMember[0]
where mUsed.ParentAssembly == assembly
select mUsed)
.DefinitionDomain
.Select(m => m.ParentType) // Don't need .Distinct() here, because of ToHashSetEx() below.
.Except(cctor.ParentType)
.Intersect(typesSuspects)
.ToHashSetEx()
)
//
// dico associates to each type suspect T, the set of types initialized (directly or indirectly)
// by the initialization of T. This second step is needed, because if a cctor of a type T1
// calls a member of a type T2, not only the cctor of T1 triggers the initialization of T2,
// but also it triggers the initialization of all types that are initialized by T2 initialization.
//
let dico = typesSuspects.Where(t => dicoTmp[t].Count() > 0).ToDictionary(
typeSuspect => typeSuspect,
typeSuspect => typeSuspect.ToEnumerable().FillIterative(
types => from t in types
from tUsed in dicoTmp[t]
select tUsed)
.DefinitionDomain
.Except(typeSuspect)
.ToHashSetEx()
)
//
// Now that dico is prepared, detect the cctor cycles
//
from t in dico.Keys
// Don't include the types generated by PostSharp
.Where(t0 => !t0.Name.Contains("z__a_"))
// Thanks to the work done to build dico, it is now pretty easy
// to spot types involved in an initialization cycle with t!
let usersAndUseds = from tTmp in dico[t]
where dico.ContainsKey(tTmp) && dico[tTmp].Contains(t)
select tTmp
where usersAndUseds.Count() > 0
// Here we've found type(s) both using and used by the suspect type.
// A cycle involving the type t is found!
// v2017.3.2: don't call Append() as an extension method else ambiguous syntax error
// with the new extension method in .NET Fx v4.7.1 / .NET Standard 2.0: System.Linq.Enumerable.Append()
let typeInitCycle = ExtensionMethodsEnumerable.Append(usersAndUseds,t)
// Compute methodsCalled and fieldsUsed, useful to explore
// how a cctor involved in a type initialization cycle, triggers other type initialization.
let methodsCalledDepth = assembly.ChildMethods.DepthOfIsUsedBy(t.ClassConstructor)
let fieldsUsedDepth = assembly.ChildFields.DepthOfIsUsedBy(t.ClassConstructor)
let methodsCalled = methodsCalledDepth.DefinitionDomain.OrderBy(m => methodsCalledDepth[m]).ToArray()
let fieldsUsed = fieldsUsedDepth.DefinitionDomain.OrderBy(f => fieldsUsedDepth[f]).ToArray()
// Use the tick box to: Group cctors methods By parent types
select new {
t.ClassConstructor,
cctorsCycle= typeInitCycle.Select(tTmp => tTmp.ClassConstructor).Except(t.ClassConstructor),
// methodsCalled and fieldsUsed are members used directly and indirectly by the cctor.
// Export these members to the dependency graph (right click the cell Export/Append … to the Graph)
// and see how the cctor trigger the initialization of other types
methodsCalled,
fieldsUsed,
Debt = (20+10*typeInitCycle.Count()).ToMinutes().ToDebt(),
Severity = Severity.Critical
}
//<Expl>
// The *{0}* and the other{1?:s} *{1}* are involved in a cyclic call,
// resulting in *unpredictable initialization order* and potential unexpected states.
//</Expl>
//<Desc>
// The *class constructor* (*static constructor*, *cctor* in IL) runs the
// first time a type is used; a static field inline initialization is enough
// to declare it implicitly.
//
// If the *cctor* of *t1* uses *t2* and vice-versa (cycles can span *N*
// types), unexpected hard-to-diagnose initialization bugs can occur. More:
// http://codeblog.jonskeet.uk/2012/04/07/type-initializer-circular-dependencies/
//
// This rule enumerates such cycles. Lambdas defined in *cctors* (or methods
// they call) are considered executed at initialization time, which may cause
// *false positives*.
//</Desc>
//<HowToFix>
// If several states held by several classes must be initialized on first
// access of any of them, create a dedicated class that initializes and
// holds all these states.
//
// Estimated Debt: 20 minutes per cycle plus 10 minutes per class constructor
// involved.
//</HowToFix>
Open in the interactive Rules Explorer · All default rules
Object Oriented Design: all 14 items
- Avoid interfaces too big ND1200
- Base class should not use derivatives ND1201
- Class shouldn't be too deep in inheritance tree ND1202
- Class with no descendant should be sealed if possible ND1203
- Overrides of Method() should call base.Method() ND1204
- Do not hide base class methods ND1205
- A stateless class or structure might be turned into a static type ND1206
- Non-static classes should be instantiated or turned to static ND1207
- Methods should be declared static if possible ND1208
- Constructor should not call a virtual method ND1209
- Avoid the Singleton pattern ND1210
- Don't assign static fields from instance methods ND1211
- Avoid empty interfaces ND1212
- Avoid types initialization cycles ND1213