Avoid types initialization cycles

Rule ND1213 Object Oriented Design

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.

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.

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>

Object Oriented Design: all 14 items