Potentially Dead Methods

Rule ND1701 Dead Code

Lists potentially dead methods: removable because never called by the program. It matches methods not called anywhere and methods called only by such methods — hence the MethodsCallingMe column and the depth metric (0 = not called, 1 = called only by uncalled methods, etc…).

Public methods are not matched by default (client code not analyzed by NDepend might call them); easily changed.

Calls from test code (the TestCode view) keep a method alive: a method called only from tests is not matched here but by the rule Code used only by tests, disabled by default.

How to fix

Static analysis cannot list dead methods exactly: methods can be invoked dynamically (like through reflection). Investigate each match; if truly never invoked, remove it to avoid maintaining useless code, or comment it out with a note on future intentions.

If an invoked method is still matched, tag it with IsNotDeadCodeAttribute from NDepend.API.dll (or your own attribute, adapting the rule).

Debt is 10 minutes (quick investigation) plus 3 minutes per unit of depth: dead methods only called by dead code take longer to investigate. Annual Interest is proportional to the method's #lines of code: the bigger, the more it slows down maintenance.

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>Potentially Dead Methods</Name>
// <Id>ND1701:PotentiallyDeadMethods</Id>

warnif count > 0
// Filter procedure for methods that shouldn't be considered as dead
let canMethodBeConsideredAsDeadProc = new Func<IMethod, bool>(
    m => !m.IsPubliclyVisible &&       // Public methods might be used by client applications of your assemblies.
         !m.IsEntryPoint &&            // Main() method is not used by-design.
         !m.IsExplicitInterfaceImpl && // The IL code never explicitly calls explicit interface methods implementation.
         !m.IsClassConstructor &&      // The IL code never explicitly calls class constructors.
         !m.IsFinalizer &&             // The IL code never explicitly calls finalizers.
         !m.IsVirtual &&               // Only check for non virtual method that are not seen as used in IL.
         !(m.IsConstructor &&          // Don't take account of protected ctor that might be call by a derived ctors.
           m.IsProtected) &&
         !m.IsEventAdder &&            // The IL code never explicitly calls events adder/remover.
         !m.IsEventRemover &&
         !m.IsGeneratedByCompiler &&
         !m.ParentType.IsDelegate &&

         // Don't consider Global ASP.NET methods as unused
         !m.ParentType.DeriveFrom("System.Web.HttpApplication".AllowNoMatch()) &&
         
         // Methods tagged with these attributes are called by the serialization infrastructure.
         !m.HasAttribute("System.Runtime.Serialization.OnSerializingAttribute".AllowNoMatch()) &&
         !m.HasAttribute("System.Runtime.Serialization.OnDeserializingAttribute".AllowNoMatch()) &&
         !m.HasAttribute("System.Runtime.Serialization.OnSerializedAttribute".AllowNoMatch()) &&
         !m.HasAttribute("System.Runtime.Serialization.OnDeserializedAttribute".AllowNoMatch()) &&

         // If you don't want to link NDepend.API.dll, you can use your own 
         // IsNotDeadCodeAttribute or UsedImplicitlyAttribute
         // and adapt the source code of this rule.
         !m.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch()) &&
         !m.HasAttribute("JetBrains.Annotations.UsedImplicitlyAttribute".AllowNoMatch()) &&

         // Module initializers are called by the runtime at assembly load (.NET 5+),
         // never from IL: a registration hook and everything it calls is alive.
         !m.HasAttribute("System.Runtime.CompilerServices.ModuleInitializerAttribute".AllowNoMatch()) &&

         // Methods called by the Unity engine
         !m.HasAttribute("UnityEngine.RuntimeInitializeOnLoadMethodAttribute".AllowNoMatch()) &&
         !m.HasAttribute("UnityEngine.InitializeOnLoadMethodAttribute".AllowNoMatch()) &&
         !m.HasAttribute("UnityEditor.MenuItemAttribute".AllowNoMatch()) &&

         // Don't consider public getters/setters 
         // of classes that implement INotifyPropertyChanged
         // as dead code.
         !(m.IsPublic && (m.IsPropertyGetter || m.IsPropertySetter) &&
           m.ParentType.Implement("System.ComponentModel.INotifyPropertyChanged".AllowNoMatch())) &&

         // ASP.NET MVC Controller classes and methods are used only by the infrastructure
         // except methods tagged by NonAction. 
         !( (m.ParentType.SimpleName.EndsWith("Controller") ||
             m.ParentType.DeriveFrom("Microsoft.AspNetCore.Mvc.Controller".AllowNoMatch()) ||
             m.ParentType.DeriveFrom("Microsoft.AspNetCore.Mvc.ControllerBase".AllowNoMatch()) ||
             m.ParentType.HasAttribute("Microsoft.AspNetCore.Mvc.ControllerAttribute".AllowNoMatch())) && 
         !m.HasAttribute("System.Web.Mvc.NonActionAttribute".AllowNoMatch())) &&

         // Methods of classes that derive from Unity classes are used implicitly by the Unity engine
         !m.ParentType.BaseClasses.Any(bc => bc.ParentNamespace.Name.StartsWithAny("UnityEngine","UnityEditor")) &&

         // Method might be called by expression in DebuggerDisplayAttribute
         !m.ParentType.HasAttribute("System.Diagnostics.DebuggerDisplayAttribute".AllowNoMatch()) &&

         // Instance ctor in class implementing interface(s) might be used through Dependency Injection
         !(m.IsConstructor && (m.IsPublic || m.IsInternal) && !m.IsStatic && 
           (m.ParentType.NbInterfacesImplemented > 0 ||  
         // The instance ctor is not used, and its parent type is used by a type 
         // that does not use any of its members. 
         // This certainly shows the service injection pattern.   
            m.ParentType.TypesUsingMe.Any(t => !t.Methods.Any(m1 => m1.MembersUsed.Any(m2 => m2.ParentType == m.ParentType)))
           ))
    )

// Get methods unused (calls from test code count: a method called only from tests
// is not dead, see the rule 'Code used only by tests')
let methodsUnused =
   from m in JustMyCode.Methods where
   m.NbMethodsCallingMe == 0 &&
   canMethodBeConsideredAsDeadProc(m)
   select m

// Dead methods = methods used only by unused methods (recursive)
let deadMethodsMetric = methodsUnused.FillIterative(
   methods => // Unique loop, just to let a chance to build the hashset.
              from o in (new object()).ToEnumerable()
              // Use a hashet to make Intersect calls much faster!
              let hashset = methods.ToHashSetEx()
              from m in codeBase.Application.Methods.UsedByAny(methods).Except(methods)
              where canMethodBeConsideredAsDeadProc(m) &&
                    // Select methods called only by methods already considered as dead
                    m.MethodsCallingMe.All(mCaller => hashset.Contains(mCaller))
              select m)

from m in JustMyCode.Methods.Intersect(deadMethodsMetric.DefinitionDomain)
let depth = deadMethodsMetric[m]
// A method of depth > 0 is called, but only by methods themselves dead: the explanation names them
let nbCallers = m.MethodsCallingMe.Count()
let deadCallers = m.MethodsCallingMe.Take(3).Select(mCaller => "*" + mCaller.ParentType.Name + "." + mCaller.Name + "*").Aggregate(", ") +
                  (nbCallers > 3 ? " and " + (nbCallers - 3).ToString() + " other" + (nbCallers > 4 ? "s" : "") : "")
select new {
   m,
   depth,
   m.MethodsCallingMe,
   expl = depth == 0 ?
      "is not invoked within the context of analyzed code and may be a candidate for removal" :
      "is called only by the method" + (nbCallers > 1 ? "s " : " ") + deadCallers + ", potentially dead too, " +
      "and may be a candidate for removal along with " + (nbCallers > 1 ? "them" : "it"),
   Debt = (10 + 3*depth).ToMinutes().ToDebt(),
   AnnualInterest = (8 + (m.NbLinesOfCode ?? 1)).ToMinutes().ToAnnualInterest()
}

//<Expl>
// The *{0}* {3}.
//</Expl>
      
//<Desc>
// Lists *potentially* **dead methods**: removable because never called by
// the program. It matches methods not called anywhere and methods called
// only by such methods — hence the *MethodsCallingMe* column and the *depth*
// metric (*0* = not called, *1* = called only by uncalled methods, etc…).
//
// *Public* methods are not matched by default (client code not analyzed by
// NDepend might call them); easily changed.
//
// Calls from **test code** (the *TestCode* view) keep a method alive: a
// method called only from tests is not matched here but by the rule *Code
// used only by tests*, disabled by default.
//</Desc>

//<HowToFix>
// *Static analysis* cannot list dead methods *exactly*: methods can be
// invoked *dynamically* (like through reflection). Investigate each match;
// if truly never invoked, remove it to avoid maintaining useless code, or
// comment it out with a note on future intentions.
//
// If an invoked method is still matched, tag it with
// **IsNotDeadCodeAttribute** from *NDepend.API.dll* (or your own attribute,
// adapting the rule).
//
// **Debt** is 10 minutes (quick investigation) plus 3 minutes per unit of
// depth: dead methods only called by dead code take longer to investigate.
// **Annual Interest** is proportional to the method's #lines of code: the
// bigger, the more it slows down maintenance.
//</HowToFix>

Dead Code: all 5 items