Code used only by tests

Rule ND1704 Dead Code

Lists application types, methods and fields used only from test code (the TestCode view): the program itself never uses them, only tests keep them alive. The testUsers column lists these test code users.

Such code is either dead production code whose tests were left behind, or a test helper living in the application code.

When a type is matched, its members and nested types are not matched. Elements exempted by the Potentially Dead Types/Methods/Fields rules (public API, infrastructure entry points, IsNotDeadCodeAttribute…) are not matched either.

How to fix

If the element is dead production code, remove it together with the tests that exercise it.

If it is a test helper, move it into the test code: a method that only uses members visible from the tests can become an extension method there, so its call sites stay unchanged.

If it is a deliberate test hook, or is used in ways IL analysis cannot see, tag it with IsNotDeadCodeAttribute from NDepend.API.dll.

Debt is 25 minutes for a type, 20 for a method and 15 for a field: the tests must be updated too. Annual Interest is proportional to the #lines of code and divided by 4 (2 minutes for a field), since such code is still exercised by the tests.

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>Code used only by tests</Name>
// <Id>ND1704:CodeUsedOnlyByTests</Id>

warnif count > 0

// Same exemptions as the rules Potentially Dead Types/Methods/Fields: an element
// they exempt (public API, infrastructure entry points, IsNotDeadCodeAttribute...)
// is used in ways IL analysis cannot see, not only from tests.
// Shared procedures: a CQLinq query cannot hold more than 32 attribute/base-class name predicates.

// 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.
let hasExemptionAttributeProc = new Func<IAttributeTarget, bool>(
   x => x.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch()) ||
        x.HasAttribute("JetBrains.Annotations.UsedImplicitlyAttribute".AllowNoMatch()))

// ASP.NET MVC Controller classes and their methods are used by the infrastructure.
let isControllerProc = new Func<IType, bool>(
   t => t.SimpleName.EndsWith("Controller") ||
        t.DeriveFrom("Microsoft.AspNetCore.Mvc.Controller".AllowNoMatch()) ||
        t.DeriveFrom("Microsoft.AspNetCore.Mvc.ControllerBase".AllowNoMatch()) ||
        t.HasAttribute("Microsoft.AspNetCore.Mvc.ControllerAttribute".AllowNoMatch()))

// Methods called by the runtime (.NET 5+ module initializers) or by the Unity engine,
// never from IL: such a method and its parent type are alive.
let isCalledByRuntimeProc = new Func<IMethod, bool>(
   m => m.HasAttribute("System.Runtime.CompilerServices.ModuleInitializerAttribute".AllowNoMatch()) ||
        m.HasAttribute("UnityEngine.RuntimeInitializeOnLoadMethodAttribute".AllowNoMatch()) ||
        m.HasAttribute("UnityEngine.InitializeOnLoadMethodAttribute".AllowNoMatch()) ||
        m.HasAttribute("UnityEditor.MenuItemAttribute".AllowNoMatch()))

let canTypeBeMatchedProc = new Func<IType, bool>(
   t => !t.IsPublic && //   Public types might be used by client applications of your assemblies.
         t.Name != "Program" &&
        !t.IsGeneratedByCompiler &&
        !t.IsUsedFromAssemblyAttributes &&
        !hasExemptionAttributeProc(t) &&
        !t.Methods.Any(m => hasExemptionAttributeProc(m) || isCalledByRuntimeProc(m)) &&
        !t.Properties.Any(p => hasExemptionAttributeProc(p)) &&

         // Exclude static types that define only const fields
         // because they cannot be seen as used in IL code.
        !(t.IsStatic && t.NbMethods == 0 && !t.Fields.Where(f => !f.IsLiteral).Any()) &&

         // Entity Framework ModelSnapshot classes are used only by the EF infrastructure.
        !t.DeriveFrom("Microsoft.EntityFrameworkCore.Infrastructure.ModelSnapshot".AllowNoMatch()) &&

        !isControllerProc(t) &&

        // TypeConverter classes are instantiated by the BCL from the [TypeConverter] attribute,
        // often through its string form ("Ns.X, Asm") that IL analysis cannot see.
        !t.DeriveFrom("System.ComponentModel.TypeConverter".AllowNoMatch()) &&

        // Types used by the Unity engine
        !t.HasAttribute("UnityEngine.RuntimeInitializeOnLoadAttribute".AllowNoMatch()) &&
        !t.HasAttribute("UnityEngine.InitializeOnLoadAttribute".AllowNoMatch()) &&
        !t.DeriveFrom("UnityEditor.AssetPostprocessor".AllowNoMatch())
)

let canMethodBeMatchedProc = 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 &&
         !hasExemptionAttributeProc(m) &&
         !isCalledByRuntimeProc(m) &&

         // 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()) &&

         // 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())) &&

         // Controller methods are used only by the infrastructure except methods tagged by NonAction.
         !(isControllerProc(m.ParentType) &&
           !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)))
           ))
    )

let canFieldBeMatchedProc = new Func<IField, bool>(
   f => !f.IsPublic &&     // Although not recommended, public fields might be used by client applications of your assemblies.
        !f.IsLiteral &&    // The IL code never explicitly uses literal fields.
        !f.IsEnumValue &&  // The IL code never explicitly uses enumeration value.
         f.Name != "value__"  && // Field named 'value__' are relative to enumerations and the IL code never explicitly uses them.
        !f.IsGeneratedByCompiler &&
        !hasExemptionAttributeProc(f) &&

        // Don't warn for fields tagged with these Unity specific attribute
        !f.HasAttribute("UnityEngine.SerializeField".AllowNoMatch()) &&
        !f.HasAttribute("UnityEngine.SerializeReference".AllowNoMatch()) &&

        // Don't warn about InlineArrayAttribute's field
        !f.ParentType.HasAttribute("System.Runtime.CompilerServices.InlineArrayAttribute".AllowNoMatch())
)

// Types used, but only from test code (the TestCode view).
// Usages from the type's own nested types don't count.
let typesUsedOnlyByTests = (
   from t in JustMyCode.Types
   let users = t.TypesUsingMe.Where(tUser => !t.NestedTypes.Contains(tUser))
   where users.Any() && users.All(tUser => tUser.IsTestCode) &&
         canTypeBeMatchedProc(t)
   select t).ToHashSetEx()

// A matched type hides its members and nested types: removing or moving the type fixes them too.
let isInMatchedTypeProc = new Func<IType, bool>(
   t => typesUsedOnlyByTests.Contains(t) || t.OutterTypes.Any(tOuter => typesUsedOnlyByTests.Contains(tOuter)))

let types = typesUsedOnlyByTests.Where(t => !t.OutterTypes.Any(tOuter => typesUsedOnlyByTests.Contains(tOuter)))

// Methods called, but only from test code
let methods =
   from m in JustMyCode.Methods where
   m.NbMethodsCallingMe > 0 && m.MethodsCallingMe.All(mCaller => mCaller.IsTestCode) &&
   !isInMatchedTypeProc(m.ParentType) &&
   canMethodBeMatchedProc(m)
   select m

// Fields used, but only from test code
let fields =
   from f in JustMyCode.Fields where
   f.NbMethodsUsingMe > 0 && f.MethodsUsingMe.All(mUser => mUser.IsTestCode) &&
   !isInMatchedTypeProc(f.ParentType) &&
   canFieldBeMatchedProc(f)
   select f

from member in types.Cast<IMember>().Concat(methods).Concat(fields)
let testUsers = member.IsType   ? member.AsType.TypesUsingMe.Where(tUser => tUser.IsTestCode).Cast<ICodeElement>() :
                member.IsMethod ? member.AsMethod.MethodsCallingMe.Cast<ICodeElement>() :
                                  member.AsField.MethodsUsingMe.Cast<ICodeElement>()
let loc = member.IsType ? member.AsType.NbLinesOfCode : member.IsMethod ? member.AsMethod.NbLinesOfCode : null
select new {
   member,
   testUsers,
   Debt = (member.IsType ? 25 : member.IsMethod ? 20 : 15).ToMinutes().ToDebt(),
   AnnualInterest = (member.IsType ? (10 + (loc ?? 1)) / 4 : member.IsMethod ? (8 + (loc ?? 1)) / 4 : 2).ToMinutes().ToAnnualInterest()
}

//<Expl>
// The *{0}* is used only from test code: the program never uses it, only tests keep it alive.
//</Expl>

//<Desc>
// Lists application **types, methods and fields used only from test code**
// (the *TestCode* view): the program itself never uses them, only tests
// keep them alive. The *testUsers* column lists these test code users.
//
// Such code is either dead production code whose tests were left behind,
// or a test helper living in the application code.
//
// When a type is matched, its members and nested types are not matched.
// Elements exempted by the *Potentially Dead Types/Methods/Fields* rules
// (public API, infrastructure entry points, *IsNotDeadCodeAttribute*…) are
// not matched either.
//</Desc>

//<HowToFix>
// If the element is dead production code, remove it together with the
// tests that exercise it.
//
// If it is a test helper, move it into the test code: a method that only
// uses members visible from the tests can become an extension method there,
// so its call sites stay unchanged.
//
// If it is a deliberate test hook, or is used in ways IL analysis cannot
// see, tag it with **IsNotDeadCodeAttribute** from *NDepend.API.dll*.
//
// **Debt** is 25 minutes for a type, 20 for a method and 15 for a field:
// the tests must be updated too. **Annual Interest** is proportional to the
// #lines of code and divided by 4 (2 minutes for a field), since such code
// is still exercised by the tests.
//</HowToFix>

Dead Code: all 5 items