Analyzing Spring .NET with NDepend3

I last took a close look at NDepend over 18 months ago back in December of 2008 in this blog post.  At that time, I spent some time looking at some of its higher-level features and shared some of my impressions of the tool.  Now, some time later I’ve returned to the tool and I just have to say – “WOW, this thing just keeps getting better and better in ways I didn’t think possible!”

When I accepted my new position with SpringSource, I realized that my need to rapidly become familiar with the Spring .NET codebase provided me with a perfect opportunity to leverage NDepend 3.  In this blog post I’m going to try to help you get a better feel for some of the finer points of NDepend 3 while using it to explore and become familiar with the structure and design of the Spring .NET codebase.

“Installing” NDepend

One of the first things you realize when it comes time to install NDepend is that there is no installer!  Instead, NDepend is one of the few .NET programs out there on the market that actually bought into the whole “XCopy Deploy” meme that .NET 1.0 was so vocal about (and has since been an idea mostly lost somewhere along the way with the growth of everything from third-party commercial installer tools to WIX to the official line from Redmond being “No .msi?  No uninstaller?  Then no Windows-logo-compatibility for you!”).

Instead of installing it, just download the ZIP file, extract it somewhere (anywhere!) and double-click the .exe to launch it.  A breath of fresh air in re: simplicity of installation, but frankly this is probably more ‘safe’ in the context of this kind of application because its target market is software developers.  The kind of things an installer would do (check for prerequisites like having the .NET framework installed, etc.) are likely things that its users would already know they would need.

Launching NDepend for the first time

imageWell, technically this is “Launching VisualNDepend for the first time”, but bear with me here for a second.

NDepend itself is really two different pieces of software (perhaps even more than two if you start to count its add-ins for things like Visual Studio and RedGate’s .NET Reflector, but more on that later). 

One piece, the NDepend console application, is used to perform the analysis of your assemblies (and to persist the results of that analysis as an XML-based “report”) and the other part, VisualNDepend, can be thought of as a sort of ‘visual explorer’ for those XML-based “reports”.  But since launching VisualNDepend is probably the first step most will take after completing the “install”, let’s start there.

The first thing you will notice about VisualNDepend’s ‘startup state’ is that its very well-designed to get new users up to speed and using the product with with a minimum of difficulty.  This is something that lots of software could take a cue from: consider what are likely the obvious questions a new user to your product would be asking and provide some easy-to-access guidance for them to help the user along.

In the case of VisualNDepend, this is provided for in a simple “Getting Started” task box that offers both on-click access to the extremely comprehensive and well-authored integrated help system and also to several videos (hosted on Amazon’s S3) that introduce the core concepts and initial steps required to get up and running with the product:

image

One of the (several) things that helps to distinguish this release of NDepend from its prior versions is the extremely high level of integration with Visual Studio that it provides.  The start screen for NDepend makes installing this dirt-simple for the new user as well – simply invoke the installer process from the obvious button below:

image

As you can see from the installation options dialog box, support is available for VS 2005, 2008, and 2010 and each offers a choice between Full Integration and Light Integration.

image

Prior to NDepend 3, Visual Studio integration was mostly limited to navigating back and forth between classes and methods in Visual NDepend and related source code in Visual Studio.  Beginning with this release however, the integration story with Visual Studio becomes much more compelling.  Light Integration is still mostly about navigating in code between a running instance of VisualNDepend and a running instance of Visual Studio although you can actually also perform CQL queries from inside Visual Studio as well as a few other so-called light-weight tasks.  Still, the Full Integration option is where I expect most software developers will want to operate as it offers nearly the entire set of capabilities of the VisualNDepend product from within the Visual Studio shell directly.

Astute readers will notice from the screenshot above that the authors of NDepend have even provided a hyperlink to an webpage with short video on getting started with Visual Studio integration.  This is just another example of some of the really thoughtful nuances of NDepend that are designed to improve the ease-of-use of an otherwise potentially quite complicated product.

Analyzing Dependencies in Spring .NET

One of the all-time best uses for a static code analysis tool like NDepend is to use the program as an exploratory tool as you “wander” through an unfamiliar code-base.  The graphs and other visualizations of the relationships between the components of the solution make the project much more comprehensible.

As an example, the following screenshot shows the dependency graph for the primary assemblies of Spring .NET:

image

This visualization makes it easy to see that the ‘foundation’ of Spring .NET is the Spring.Core project.  Spring.Aop, Spring.Web, and Spring.Services all have a direct dependency on Spring.Core as does Spring.Template.Velocity.  Because in this particular graph the area (size) of the boxes reflects relative number of lines of code in each project, we can see that of these immediate dependencies Spring.Aop and Spring.Web are the largest project with Spring.Services following closely behind but that Spring.Template.Velocity is a much smaller project than the others at this dependency level.

Spring.Data and Spring.Web.Extensions follow on in the next tier of dependencies followed then by a plethora of smaller ‘subsystems’ in Spring .NET such as the support libraries for the various versions of NHibernate, Messaging support libraries, Scheduling, and more.

This is more or less what one would expect of a set of framework libraries, of course – common elements at the bottom, more powerful but still general abstractions built atop that core, followed closely by ever-more-specific functional libraries atop that.  But a graph like this one – and the fact that its possible to just point NDepend at the Spring .NET assemblies and have it rapidly generate this visual is a tremendous help in starting to understand the dependency structures involved in the projects.

Identifying Potential Pain Points in Spring .NET

One of the things at which NDepend excels is in gathering and analyzing so-called ‘code metrics’.  I mentioned CQL, the Code-Query-Language, supported by NDepend in my last post about v2.x and its still present in v3 too of course.  NDepend ships with a set of very useful pre-defined queries (and you can can create your own additional queries in CQL and save/retrieve them too).

Some of the most interesting (to me) to use when inspecting an unfamiliar codebase are those in the ‘code quality’ category.  Here I’m selecting the 10 methods that have been found to have the highest cyclomatic complexity (CC) metric.

image

…and here we see the list of those 10 methods in Spring .NET having a CC of > 20…

image

The one highlighted at the top is of particular concern to me as its got a near-unbelievable CC value of 124!  This certainly makes this method stand out like the proverbial ‘sore thumb’, making it worthy of further investigation.  Fortunately, NDepend makes such exploration amazingly easy – simply double-clicking on the method name in the CQL output window immediately opens the method’s source file in the code editor window for me.  NDepend knows how to navigate the codebase to take me directly to where I want to go.

For fun, here’s this actual method from the Spring.Expressions.Parser.ExpressionLexer class…

   1: public void mID(bool _createToken) //throws RecognitionException, CharStreamException, TokenStreamException

   2: {

   3:         int _ttype; IToken _token=null; int _begin=text.Length;

   4:         _ttype = ID;

   5:         

   6:         {

   7:             switch ( cached_LA1 )

   8:             {

   9:             case 'a':  case 'b':  case 'c':  case 'd':

  10:             case 'e':  case 'f':  case 'g':  case 'h':

  11:             case 'i':  case 'j':  case 'k':  case 'l':

  12:             case 'm':  case 'n':  case 'o':  case 'p':

  13:             case 'q':  case 'r':  case 's':  case 't':

  14:             case 'u':  case 'v':  case 'w':  case 'x':

  15:             case 'y':  case 'z':

  16:             {

  17:                 matchRange('a','z');

  18:                 break;

  19:             }

  20:             case 'A':  case 'B':  case 'C':  case 'D':

  21:             case 'E':  case 'F':  case 'G':  case 'H':

  22:             case 'I':  case 'J':  case 'K':  case 'L':

  23:             case 'M':  case 'N':  case 'O':  case 'P':

  24:             case 'Q':  case 'R':  case 'S':  case 'T':

  25:             case 'U':  case 'V':  case 'W':  case 'X':

  26:             case 'Y':  case 'Z':

  27:             {

  28:                 matchRange('A','Z');

  29:                 break;

  30:             }

  31:             case '_':

  32:             {

  33:                 match('_');

  34:                 break;

  35:             }

  36:             default:

  37:             {

  38:                 throw new NoViableAltForCharException(cached_LA1, getFilename(), getLine(), getColumn());

  39:             }

  40:              }

  41:         }

  42:         {    // ( ... )*

  43:             for (;;)

  44:             {

  45:                 switch ( cached_LA1 )

  46:                 {

  47:                 case 'a':  case 'b':  case 'c':  case 'd':

  48:                 case 'e':  case 'f':  case 'g':  case 'h':

  49:                 case 'i':  case 'j':  case 'k':  case 'l':

  50:                 case 'm':  case 'n':  case 'o':  case 'p':

  51:                 case 'q':  case 'r':  case 's':  case 't':

  52:                 case 'u':  case 'v':  case 'w':  case 'x':

  53:                 case 'y':  case 'z':

  54:                 {

  55:                     matchRange('a','z');

  56:                     break;

  57:                 }

  58:                 case 'A':  case 'B':  case 'C':  case 'D':

  59:                 case 'E':  case 'F':  case 'G':  case 'H':

  60:                 case 'I':  case 'J':  case 'K':  case 'L':

  61:                 case 'M':  case 'N':  case 'O':  case 'P':

  62:                 case 'Q':  case 'R':  case 'S':  case 'T':

  63:                 case 'U':  case 'V':  case 'W':  case 'X':

  64:                 case 'Y':  case 'Z':

  65:                 {

  66:                     matchRange('A','Z');

  67:                     break;

  68:                 }

  69:                 case '_':

  70:                 {

  71:                     match('_');

  72:                     break;

  73:                 }

  74:                 case '0':  case '1':  case '2':  case '3':

  75:                 case '4':  case '5':  case '6':  case '7':

  76:                 case '8':  case '9':

  77:                 {

  78:                     matchRange('0','9');

  79:                     break;

  80:                 }

  81:                 default:

  82:                 {

  83:                     goto _loop166_breakloop;

  84:                 }

  85:                  }

  86:             }

  87: _loop166_breakloop:            ;

  88:         }    // ( ... )*

  89:         _ttype = testLiteralsTable(_ttype);

  90:         if (_createToken && (null == _token) && (_ttype != Token.SKIP))

  91:         {

  92:             _token = makeToken(_ttype);

  93:             _token.setText(text.ToString(_begin, text.Length-_begin));

  94:         }

  95:         returnToken_ = _token;

  96:     }

As you can see, the seemingly-impossible-to-achieve CC value of 124 🙂 is largely due to the approach of having a switch condition for every letter of the alphabet (both uppercase and lowercase) and another for every digit from 0-9 in this single method.  Although this does technically result in an absurdly high CC analysis metric for this method, a quick review of why this method has so high a CC value tells me that its really not as difficult to comprehend this method as a CC value of 124 would suggest at first-glance.

This is a big part of the value of having a tool like NDepend integrated so well right into the IDE in this manner: you can quickly investigate and validate such statistical outliers when they come to your attention to determine whether they are worth taking corrective action to address.

As a complete ‘aside’, you can also infer that this method was ported from the Spring Framework for Java since you can see how the checked exceptions syntax has been left intact from the port at the top of the method declaration (though obviously its been commented out since – as we all know and has been thoroughly debated by many – the C# language doesn’t have support for checked exceptions!).  Personally I find it quite interesting that while the initial design decision not to include support for Checked Exception in C# is vigorously defended by the language’s author(s), in .NET 4 we’ve now got support for Code Contracts in a way that at least tries to solve the same kinds of troubles in the code that Checked Exceptions were intended to solve, but that’s probably a post for another day…

In addition to cyclomatic complexity being a possible indication of long-term maintenance concerns for the codebase, another great metric to look at is Source Lines of Code (SLOC) in each method.  Fortunately, as seen in the following screenshot NDepend ships with a CQL query that covers that too…

image

The names of the top ten longest methods in the Spring .NET code are shown the CQL results pane for further investigation:

image

That method with the 153 lines is initially a candidate for further investigation – I sure wouldn’t want to have to read through a 153-line long method to try to understand what its doing before attempting to fix a bug or add additional functionality to it!  Just as before, NDepend makes it simple to just double-click on the method in the CQL results pane and jump right to the code:

   1: override public IToken nextToken()            //throws TokenStreamException

   2:         {

   3:             IToken theRetToken = null;

   4: tryAgain:

   5:             for (;;)

   6:             {

   7:                 IToken _token = null;

   8:                 int _ttype = Token.INVALID_TYPE;

   9:                 resetText();

  10:                 try     // for char stream error handling

  11:                 {

  12:                     try     // for lexical error handling

  13:                     {

  14:                         switch ( cached_LA1 )

  15:                         {

  16:                         case '\t':  case '\n':  case '\r':  case ' ':

  17:                         {

  18:                             mWS(true);

  19:                             theRetToken = returnToken_;

  20:                             break;

  21:                         }

  22:                         case '@':

  23:                         {

  24:                             mAT(true);

  25:                             theRetToken = returnToken_;

  26:                             break;

  27:                         }

  28:                         case '`':

  29:                         {

  30:                             mBACKTICK(true);

  31:                             theRetToken = returnToken_;

  32:                             break;

  33:                         }

  34:                         case '|':

  35:                         {

  36:                             mPIPE(true);

  37:                             theRetToken = returnToken_;

  38:                             break;

  39:                         }

  40:                         case '#':

  41:                         {

  42:                             mPOUND(true);

  43:                             theRetToken = returnToken_;

  44:                             break;

  45:                         }

  46:                         case '(':

  47:                         {

  48:                             mLPAREN(true);

  49:                             theRetToken = returnToken_;

  50:                             break;

  51:                         }

  52:                         case ')':

  53:                         {

  54:                             mRPAREN(true);

  55:                             theRetToken = returnToken_;

  56:                             break;

  57:                         }

  58:                         case '[':

  59:                         {

  60:                             mLBRACKET(true);

  61:                             theRetToken = returnToken_;

  62:                             break;

  63:                         }

  64:                         case ']':

  65:                         {

  66:                             mRBRACKET(true);

  67:                             theRetToken = returnToken_;

  68:                             break;

  69:                         }

  70:                         case '}':

  71:                         {

  72:                             mRCURLY(true);

  73:                             theRetToken = returnToken_;

  74:                             break;

  75:                         }

  76:                         case ',':

  77:                         {

  78:                             mCOMMA(true);

  79:                             theRetToken = returnToken_;

  80:                             break;

  81:                         }

  82:                         case ';':

  83:                         {

  84:                             mSEMI(true);

  85:                             theRetToken = returnToken_;

  86:                             break;

  87:                         }

  88:                         case ':':

  89:                         {

  90:                             mCOLON(true);

  91:                             theRetToken = returnToken_;

  92:                             break;

  93:                         }

  94:                         case '+':

  95:                         {

  96:                             mPLUS(true);

  97:                             theRetToken = returnToken_;

  98:                             break;

  99:                         }

 100:                         case '-':

 101:                         {

 102:                             mMINUS(true);

 103:                             theRetToken = returnToken_;

 104:                             break;

 105:                         }

 106:                         case '/':

 107:                         {

 108:                             mDIV(true);

 109:                             theRetToken = returnToken_;

 110:                             break;

 111:                         }

 112:                         case '*':

 113:                         {

 114:                             mSTAR(true);

 115:                             theRetToken = returnToken_;

 116:                             break;

 117:                         }

 118:                         case '%':

 119:                         {

 120:                             mMOD(true);

 121:                             theRetToken = returnToken_;

 122:                             break;

 123:                         }

 124:                         default:

 125:                             if ((cached_LA1=='?') && (cached_LA2=='?'))

 126:                             {

 127:                                 mDEFAULT(true);

 128:                                 theRetToken = returnToken_;

 129:                             }

 130:                             else if ((cached_LA1=='=') && (cached_LA2=='=')) {

 131:                                 mEQUAL(true);

 132:                                 theRetToken = returnToken_;

 133:                             }

 134:                             else if ((cached_LA1=='!') && (cached_LA2=='=')) {

 135:                                 mNOT_EQUAL(true);

 136:                                 theRetToken = returnToken_;

 137:                             }

 138:                             else if ((cached_LA1=='<') && (cached_LA2=='=')) {

 139:                                 mLESS_THAN_OR_EQUAL(true);

 140:                                 theRetToken = returnToken_;

 141:                             }

 142:                             else if ((cached_LA1=='>') && (cached_LA2=='=')) {

 143:                                 mGREATER_THAN_OR_EQUAL(true);

 144:                                 theRetToken = returnToken_;

 145:                             }

 146:                             else if ((cached_LA1=='!') && (cached_LA2=='{')) {

 147:                                 mPROJECT(true);

 148:                                 theRetToken = returnToken_;

 149:                             }

 150:                             else if ((cached_LA1=='?') && (cached_LA2=='{')) {

 151:                                 mSELECT(true);

 152:                                 theRetToken = returnToken_;

 153:                             }

 154:                             else if ((cached_LA1=='^') && (cached_LA2=='{')) {

 155:                                 mSELECT_FIRST(true);

 156:                                 theRetToken = returnToken_;

 157:                             }

 158:                             else if ((cached_LA1=='$') && (cached_LA2=='{')) {

 159:                                 mSELECT_LAST(true);

 160:                                 theRetToken = returnToken_;

 161:                             }

 162:                             else if ((cached_LA1=='T') && (cached_LA2=='(')) {

 163:                                 mTYPE(true);

 164:                                 theRetToken = returnToken_;

 165:                             }

 166:                             else if ((cached_LA1=='{') && (cached_LA2=='|')) {

 167:                                 mLAMBDA(true);

 168:                                 theRetToken = returnToken_;

 169:                             }

 170:                             else if ((cached_LA1=='\\') && (cached_LA2=='.')) {

 171:                                 mDOT_ESCAPED(true);

 172:                                 theRetToken = returnToken_;

 173:                             }

 174:                             else if ((cached_LA1=='\'') && ((cached_LA2 >= '\u0000' && cached_LA2 <= '\ufffe'))) {

 175:                                 mSTRING_LITERAL(true);

 176:                                 theRetToken = returnToken_;

 177:                             }

 178:                             else if ((cached_LA1=='0') && (cached_LA2=='x')) {

 179:                                 mHEXADECIMAL_INTEGER_LITERAL(true);

 180:                                 theRetToken = returnToken_;

 181:                             }

 182:                             else if ((cached_LA1=='\\') && (true)) {

 183:                                 mBACKSLASH(true);

 184:                                 theRetToken = returnToken_;

 185:                             }

 186:                             else if ((cached_LA1=='!') && (true)) {

 187:                                 mBANG(true);

 188:                                 theRetToken = returnToken_;

 189:                             }

 190:                             else if ((cached_LA1=='?') && (true)) {

 191:                                 mQMARK(true);

 192:                                 theRetToken = returnToken_;

 193:                             }

 194:                             else if ((cached_LA1=='$') && (true)) {

 195:                                 mDOLLAR(true);

 196:                                 theRetToken = returnToken_;

 197:                             }

 198:                             else if ((cached_LA1=='{') && (true)) {

 199:                                 mLCURLY(true);

 200:                                 theRetToken = returnToken_;

 201:                             }

 202:                             else if ((cached_LA1=='=') && (true)) {

 203:                                 mASSIGN(true);

 204:                                 theRetToken = returnToken_;

 205:                             }

 206:                             else if ((cached_LA1=='^') && (true)) {

 207:                                 mPOWER(true);

 208:                                 theRetToken = returnToken_;

 209:                             }

 210:                             else if ((cached_LA1=='<') && (true)) {

 211:                                 mLESS_THAN(true);

 212:                                 theRetToken = returnToken_;

 213:                             }

 214:                             else if ((cached_LA1=='>') && (true)) {

 215:                                 mGREATER_THAN(true);

 216:                                 theRetToken = returnToken_;

 217:                             }

 218:                             else if ((cached_LA1=='\'') && (true)) {

 219:                                 mQUOTE(true);

 220:                                 theRetToken = returnToken_;

 221:                             }

 222:                             else if ((tokenSet_0_.member(cached_LA1)) && (true)) {

 223:                                 mID(true);

 224:                                 theRetToken = returnToken_;

 225:                             }

 226:                             else if ((tokenSet_1_.member(cached_LA1)) && (true)) {

 227:                                 mNUMERIC_LITERAL(true);

 228:                                 theRetToken = returnToken_;

 229:                             }

 230:                         else

 231:                         {

 232:                             if (cached_LA1==EOF_CHAR) { uponEOF(); returnToken_ = makeToken(Token.EOF_TYPE); }

 233:                 else {throw new NoViableAltForCharException(cached_LA1, getFilename(), getLine(), getColumn());}

 234:                         }

 235:                         break; }

 236:                         if ( null==returnToken_ ) goto tryAgain; // found SKIP token

 237:                         _ttype = returnToken_.Type;

 238:                         returnToken_.Type = _ttype;

 239:                         return returnToken_;

 240:                     }

 241:                     catch (RecognitionException e) {

 242:                             throw new TokenStreamRecognitionException(e);

 243:                     }

 244:                 }

 245:                 catch (CharStreamException cse) {

 246:                     if ( cse is CharStreamIOException ) {

 247:                         throw new TokenStreamIOException(((CharStreamIOException)cse).io);

 248:                     }

 249:                     else {

 250:                         throw new TokenStreamException(cse.Message);

 251:                     }

 252:                 }

 253:             }

 254:         }

As a quick review of this longest single method in the entire Spring .NET codebase points out — similar to the method with the highest cyclomatic complexity — this longest method (153 lines!) is really only this long because of the sheer number of conditions (tokens, really) that it has to process.

What Else is New in NDepend 3?

There are two final somewhat related features that I really need to mention if you’re considering NDepend or any other structural analysis tooling.  One of them is the ability to compare analysis from different versions of the same project (e.g., compare v1 to v1.1, etc.) in order to identify not just code analysis metrics for a single snapshot in time but also to begin to understand trends in these metrics over time.  This is a tremendously powerful (and valuable) capability as it tells you not only where you project is but also whether its headed in the right direction or not on all of these very useful code metrics.

The second of these very important features is also extremely valuable: the ability to integrate code coverage reports from unit test runs into the analysis and visualizations.  This feature permits you to review code coverage alongside the other code metrics as you are reviewing them.  Find a method with a very high cyclomatic complexity?  Well, one of the factors that will probably play into your decision of whether or not that method should be a real cause for concern or not will likely be understanding how well that highly-complex method is covered by unit tests – and this feature gives you the ability to integrate that intelligence right into the NDepend analysis and visualizations.

Look for more info on my experiences with both of these additional features in a future post.

What About the VS2010 Ultimate Architect Tooling?

Those of you familiar with the so-called “Architecture” tools included in the VS2010 Ultimate edition are probably wondering “why wouldn’t I just use the tooling provided there instead of a third-party tool?”  This is a pretty good question (on the surface) but let’s explore some of the obvious and not-so-obvious answers to that question:

  • The Architecture tools in VS2010 Ultimate Edition are only available for VS2010 and even then only in the ‘Ultimate Edition’ so if you aren’t yet on VS2010 you’re out of luck there.  NDepend’s integrated VS tooling is available for VS2005 and VS2008 as well as VS2010.  And even if (by chance) you’re still on VS2003 or VS2002 (the original VS.NET release), you can still use 100% of the NDepend functionality via the external VisualNDepend application and navigate between all the diagrams and the code in your IDE the same as if you were on a more recent Visual Studio instance.
  • The cost differential between an MSDN Ultimate subscription and an MSDN Premium subscription is tremendous (over $5,000!).  Granted, there are a lot of other benefits to MSDN Ultimate that go well beyond just getting access to the Architecture tools in VS2010, but if the Architecture tools are all you really want the only way to get access to them is to pay that $5,000 per seat premium to Microsoft for the Ultimate subscription.  And that $5,000 differential gets worse of course (about $10,000 per seat) if you’re otherwise only looking at MSDN Professional subscriptions.  The NDepend pricing model (because its for *just* the Architecture-style tooling capabilities) is considerably more approachable (under $400 per seat) for the developer/team that just wants access to this singular set to tooling.
  • The Architecture tools in VS2010 Ultimate are *only* integrated into the IDE.  This means that everyone needing to analyze, view, inspect, etc. the code using these tools needs a license of Visual Studio – and needs to know how to use Visual Studio.  Since NDepend operates against the compiled binaries of your project, VisualNDepend can be launched by others involved in the project who may not be well-versed in the use of Visual Studio (or have licenses of Visual Studio allocated to them).
  • NDepend (from the console) has a strong Continuous Integration integration story (pardon the double use of ‘integration’ there!).  You can simply display NDepend’s own HTML report output as part of your CI build reporting or you can apply one or more XSLT transforms to its XML output to easily customize what you display in CI reports.  VS2010’s Architecture tools output .dgml diagrams that don’t benefit from this kind of an (easy) integration story.
  • NDepend’s idea of being able to provide ‘analysis diffs’ between one or more versions of your compiled binaries (to see changes, trends, etc. over time) isn’t really manifest anywhere in the VS2010 Architecture tooling (at least not yet!).
  • NDepend’s ability to ‘overlay’ unit test code-coverage reporting into its analysis and visual display of information is simply brilliant and also isn’t manifest anywhere in the VS2010 Architecture tooling.  And since Microsoft doesn’t have any code-coverage tool of its own AFAIK, its probably unlikely that this capability would be coming soon to VS2010 Ultimate.

Conclusion

If you’re serious about a static code analysis tool for .NET, you owe it to yourself to download the trial of NDepend 3 – its not only a tremendously valuable tool in your .NET development arsenal, but is also one of the more thoughtfully and professionally designed pieces of software I’ve had the pleasure to use in a long time!