API Breaking Changes: Methods

Rule ND1501 API Breaking Changes

Runs only in diff mode (baseline defined). Warns if a method publicly visible in the baseline is no longer public or has been removed: client code using it will be broken.

How to fix

Keep public methods public, now and in the future. If a public method must be removed, tag it with System.ObsoleteAttribute and a workaround message for a few releases first (obsolete-tagged removals are not matched).

Severity is High: an API Breaking change can provoke significant friction with API consumers.

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>API Breaking Changes: Methods</Name>
// <Id>ND1501:APIBreakingChangesMethods</Id>

warnif count > 0 from m in codeBase.OlderVersion().Application.Methods
where m.IsPubliclyVisible  

     // The method has been removed, it was not tagged as obsolete
     // and its parent type hasn't been removed …
let reason =  
  ( m.WasRemoved() && 
   !m.IsVirtual &&
   !m.ParentType.WasRemoved() && 
   !m.IsObsolete ) ? (
   m.ParentType.NewerVersion().MethodsAndConstructors.Any(m1 => m1.WasAdded() && m1.SimpleNameEquals(m.SimpleName)) ?
    "is public and has different parameters than in baseline" : 
    "was public and has been removed"
   ) :

  // … or the method is not publicly visible anymore 
  //   but the parent type is still publicly visible
  (!m.WasRemoved() && !m.NewerVersion().IsPubliclyVisible && 
    m.ParentType.NewerVersion().IsPubliclyVisible) ? 
   "used to be publicly visible but not anymore" :

  // … or the method return type has changed
  (!m.WasRemoved() && m.ReturnType != null && m.NewerVersion().ReturnType != null
                   && m.ReturnType.FullName != m.NewerVersion().ReturnType.FullName) 
  ? "is public and has a different return type" : null       
      
where reason != null

//--------------------------------------
// Handle special case: if between two versions a regular property becomes 
// an auto-property (or vice-versa) the property getter/setter method have 
// a different value for IMethod.IsGeneratedByCompiler
// since auto-property getter/setter are marked as generated by the compiler.
//
// If a method IsGeneratedByCompiler value changes between two versions, 
// NDepend doesn't pair the newer/older occurences of the method.
//
// Hence in such situation, a public method is seen as added 
// and a public method is seen as removed, but the API is not broken!
// The equivalentMethod-check below avoids reporting such 
// API Breaking Change false-positive.
let equivalentMethod = m.WasRemoved() && m.ParentType.IsPresentInBothBuilds() ?
   m.ParentType.NewerVersion().Methods
   .FirstOrDefault(m1 => 
       m1.IsPubliclyVisible &&
       m1.Name == m.Name &&
       m1.IsGeneratedByCompiler != m.IsGeneratedByCompiler &&
      (m1.ReturnType == null || m.ReturnType == null || m1.ReturnType.FullName == m.ReturnType.FullName)
    )
    : null
where equivalentMethod == null
//--------------------------------------


select new { 
   m,
   NewVisibility = 
      (m.WasRemoved() ? " " : 
       m.NewerVersion().Visibility.ToString()),
   reason,
   Debt = 10.ToMinutes().ToDebt(),
   Severity = Severity.High
}

//<Expl>The *{0}* {2}. This breaks client code using it.</Expl>

//<Desc>
// Runs only in *diff mode* (baseline defined). Warns if a method publicly
// visible in the *baseline* is no longer public or has been removed:
// client code using it will be broken.
//</Desc>

//<HowToFix>
// Keep public methods public, now and in the future. If a public method must
// be removed, tag it with *System.ObsoleteAttribute* and a *workaround
// message* for a few releases first (obsolete-tagged removals are not
// matched).
//
// Severity is **High**: an API Breaking change can provoke significant
// friction with API consumers.
//</HowToFix>

API Breaking Changes: all 9 items