Prime Programming Proficiency, Part 3: Lines-of-Code Counter

Introduction

George Lucas was quoted as saying that he originally envisioned Star Wars as a series of nine movies. The first movie (self-titled) came out in 1976, followed by The Empire Strikes Back and Return of the Jedi. Almost thirty years since the first movie was released, prequels 1, 2, and 3 came out, which makes the first three movies episodes 4, 5, and 6—I guess. It's all sort of confusing.

This is the third and final installment of the Prime Programming Proficiency series—and you didn't have to wait 30 years for it. The first installment discussed heuristics and how you can use lines of code as a rudimentary starting place for establishing efficient software development. The subject led to many more questions than answers, but questions are good beginnings. The second installment introduced the extensibility object model and macros, setting the stage for implementing a built-in, lines-of-code counter using macros and VB.NET.

This final installment uses the Visitor behavior pattern to implement this lines-of-code counter. It includes the complete code listings, making the article a bit longish, but it will help you reproduce the solution.

The Problem and Solution Summarized

The problem is trying to track the number of projects, files, and lines of code in any given solution implemented with VS.NET. The solution—stated in Part 2 as a list of imperatives—is to invoke the utility in VS.NET and implement it as a macro that other teams can share. Finally, the utility displays running sub-totals and final totals (which include project and file names, project and file counts, and lines-of-code counts for source code files) when it has examined all files. This is manageable.

Introducing the Visitor Pattern

What are design patterns? I think of them as solution blueprints. Someone else had a problem, solved it successfully (and hopefully simplified some things), factored out domain-specific information, and then published his or her ideas. A design is an instance of one or more patterns, and software is an instance of a design. It might seem strange that software preceded design, which preceded patterns, but this is what happens with most if not all things. Innovation is followed by formalization, which is a precursor to mass production, automation, and finally, obsolescence.

The Visitor pattern is called a behavior pattern because it describes a solution for how something behaves. The Visitor pattern is useful when one encounters a problem where an action needs to be performed on a group of things. The action might be as simple as modifying a property or as complex as creating new objects. Think of an after-hours security guard making rounds at a government facility. Every hour, he has to ensure that all the doors are locked, a complicated and time-consuming operation considering that the facility has several floors, hundreds of doors, and multiple buildings. Now, suppose the security guard also has to check alarm systems, safes, parking facilities, and walk Mrs. Johnson to her car. The guard is still stopping at—or visiting—each thing, but what he does when he gets there is different depending on what he's visiting. For instance, Mrs. Johnson doesn't want to be jiggled and the door can't be walked to the parking lot. If the guard's duties were a programming solution, the Visitor pattern would separate the visiting behavior—or the making of rounds—from the action that occurs during the course of the visit.

Tip: Good designs are often based on metaphorical physical processes with distinct, well-known boundaries.

In this article's problem domain, a solution, project, and file are all elements known to Visual Studio .NET. While we want to visit each, we want to do something different with each, based on its type. Because we implement our solution as a macro, we have certain constraints that must be implemented too, like a macro entry point. Let's start there.

Defining the Macro Entry Point

Design patterns do not solve all of our problems. We might use many patterns in an application, using some patterns more than once. Other things do not require patterns. For example, a macro entry point simply is a subroutine that VS.NET requires; it has little to do with the Visitor pattern. The starting point for our solution is shown in Listing 1.

Listing 1: The macro starting point.

Option Explicit Off

Imports EnvDTE
Imports System.Diagnostics
Imports System.Windows
Imports System.Windows.Forms
Imports System
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.text


Public Module MyUtilities
   Public Sub DumpProject()
      Dim i As SolutionIterator = New SolutionIterator
      i.Iterate()
   End Sub

End Module

Generally, I start working with objects and move to the user interface, which means I actually wrote the code in Listing 1 last. As you can see, the DumpProject method is very straightforward: It creates something called SolutionIterator and calls Iterate. Simple code like Listing 1 should be familiar to you; it is about the same level of complexity as the Main subroutine in Windows Forms and Console applications.

Listing 2 shows the SolutionIterator. This class and its Iterate method are also quite simple. Iterator creates an instance of a class called Visitor and a SolutionElement that is provided with the current solution.

Listing 2: The SolutionIterator class.

Imports EnvDTE
Imports System.Diagnostics

Public Class SolutionIterator

   Public Sub Iterate()
        Dim visitor As Visitor = New Visitor

      Dim solutionElement As SolutionElement = _
         New SolutionElement(DTE.Solution.Projects)
      solutionElement.Accept(visitor)

      Output.WriteLine("Total Line Count: " & visitor.LineCount)
      Output.WriteLine("Total Files: "      & visitor.ItemCount)
      Output.WriteLine("Total Projects: "   & visitor.ProjectCount)
   End Sub

End Class

After you create the solutionElement, you invoke an Accept method. When Accept returns, you should have your totals.

The pieces you need to create, as Listing 2 suggests, are the Visitor, SolutionElement class, and the Output class. The Output class is a wrapper for the VS.NET IDE's Output window, and Visitor and SolutionElement are dependent on the Visitor pattern. Now, you are getting into the meat of the solution.

Prime Programming Proficiency, Part 3: Lines-of-Code Counter

Implementing the IVisitor Interface

The Visitor pattern can be implemented using abstract classes or interfaces. I chose interfaces. The basic idea is that you define an interface with methods named Visit. For each kind of thing you want to Visit, you define an overloaded Visit method. For example, I want to the Visit SolutionElement, ItemElement, and ProjectElement types; hence, I declare a Visit method for each of these types in IVisitor. In addition, the Visitor is visiting each of these types and grabbing information from them. It will be up to the Visitor to store that information. In our example, we store project, item, and line counts. Therefore, our visitors also will need to collect and make accessible this information.

Listing 3 shows the definition of IVisitor and the implementation of an IVisitor, the Visitor class.

Listing 3: The IVisitor interface and Visitor class.

' IVisitor.vb
Imports EnvDTE
Imports System.Diagnostics

Public Interface IVisitor
   Sub Visit(ByVal element As SolutionElement)
   Sub Visit(ByVal element As ItemElement)
   Sub Visit(ByVal element As ProjectElement)
   Property ProjectCount() As Long
   Property ItemCount()    As Long
   Property LineCount()    As Long
End Interface


' Visitor.vb
Imports EnvDTE
Imports System.Diagnostics

Public Class Visitor
   Implements IVisitor

   Private FProjectCount As Long = 0
   Private FItemCount    As Long = 0
   Private FLineCount    As Long = 0

   Public Property ProjectCount() As Long Implements _
          IVisitor.ProjectCount
      Get
         Return FProjectCount
      End Get
      Set(ByVal Value As Long)
         FProjectCount = Value
      End Set
   End Property

   Public Property ItemCount() As Long Implements _
          IVisitor.ItemCount
      Get
         Return FItemCount
      End Get
      Set(ByVal Value As Long)
         FItemCount = Value
      End Set
   End Property

   Public Property LineCount() As Long Implements _
          IVisitor.LineCount
      Get
         Return FLineCount
      End Get
      Set(ByVal Value As Long)
         FLineCount = Value
      End Set
   End Property

   Public Sub VisitProject(ByVal element As ProjectElement) _
          Implements IVisitor.Visit
      element.Iterate()
   End Sub

   Public Sub VisitProjectItem(ByVal element As ItemElement) _
          Implements IVisitor.Visit
      element.Iterate()
   End Sub

   Public Sub VisitProjects(ByVal element As SolutionElement) _
          Implements IVisitor.Visit
      element.Iterate()
   End Sub
End Class

I hope you appreciate just how simple the code is. Individual pieces of code should be pretty simple. This supports the idea of divid et imperum, or divide and conquer. To divide and conquer a problem means to subdivide it into smaller, easier-to-implement pieces.

As you will see shortly, each of the prefixSolution classes implement an Iterate method that supports traversing that node's solution and project directory-based file system. For example, a ProjectItem might be a folder that contains other folders or files that require traversing that ProjectItem's elements.

Prime Programming Proficiency, Part 3: Lines-of-Code Counter

Implementing the IHost Interface

A good technique for designing solutions is to state the object and pluck out the nouns and verbs. The nouns will tell you what is acting, what is acted upon, and what the actions are. In our implementation, we simply state that hosts are something that accept visitors. Plucking the nouns and verbs we have host, visitor, something, and accept.

Something is suitable here because all kinds of things play the role of host and visitor. The current confluence in my lungs suggests that I am hosting (albeit unwanted guests) some very small visitors. Your grandmother may play host to your family during the holidays, and when my kids forgot to do laundry for a week while I was on a recent business trip my dryer vent was host to a swallow. Because something and patterns in general are generic, the word something suggests we use an interface. Hosts accept visitors, resulting in the culmination of an interface (IHost) with a method (Accept) that requires an IVisitor (see Listing 4 for an implementation of IHost).

Note: Some implementations of the visitor pattern might use Visitor and Visitable. This has a nice symmetry, but Visitor and Host sound a little better. Precise naming is not important. Clarity is sufficient.

Listing 4: The IHost interface.

Imports EnvDTE
Imports System.Diagnostics

Public Interface IHost

   Sub Accept(ByVal visitor As IVisitor)

End Interface

Every class that accepts a visitor needs to implement IHost. As mentioned previously, the Host is capable of doing or knowing something about the Visitor's wants and needs. In this example, the visitors want to gather count heuristics from each host.

Implementing hosts

The extensibility object model was not implemented with the Visitor pattern. Thus to make ProjectItem, Project, and Projects (part of the extensibility object model) work with our pattern, we need to wrap these elements in classes that do work with the Visitor, that do implement IHost. I defined the following three classes for this purpose:

  • SolutionElement represents the wrapper for a solution, containing a reference to the extensibility object model's Projects collection.
  • ProjectElement represents the wrapper for a Project.
  • ItemElement wraps ProjectItem.

Listings 5, 6, and 7 show the implementation of each class, respectively. Each listing is followed by a brief summary showcasing the finer points.

Listing 5: The SolutionElement.

Imports EnvDTE
Imports System.Diagnostics

Public Class SolutionElement
   Implements IHost

   Private FProjects As Projects
   Private FVisitor As IVisitor

   Public Sub New(ByVal projects As Projects)
      FProjects = projects
   End Sub

   Public ReadOnly Property Projects() As Projects
      Get
         Return FProjects
      End Get
   End Property

   Public Sub Accept(ByVal visitor As IVisitor) _
          Implements IHost.Accept
      FVisitor = visitor
      visitor.Visit(Me)
   End Sub

   Public Sub Iterate()
      Output.Clear()
      Output.WriteLine(DTE.Solution.FullName)

      Dim project As Project
      Dim projectElement As ProjectElement = New ProjectElement

      For Each project In FProjects
         projectElement.CurrentProject = project
         projectElement.Accept(FVisitor)
      Next
   End Sub

End Class

Prime Programming Proficiency, Part 3: Lines-of-Code Counter

Listing 6 is the SolutionElement. It implements IHost and is initialized with a Projects collection when the constructor—Sub New—is called. The Accept method implements IHost and accepts a visitor. The Iterate method does the interesting work. Iterate clears the Output device (see the Implementing the Output Window section later in this article), writes the name of the current solution, and then iterates over each Project in the Projects collection.

Notice that I create only one instance of ProjectElement and reuse it inside the For Each loop. Because it is a wrapper and its state is transient, this conservative code will reduce memory chunking. Within the loop, the ProjectElement is told which ProjectItem it currently represents and then the ProjectElement is visited.

Listing 6: The ProjectElement.

Imports EnvDTE
Imports System
Imports System.Diagnostics

Public Class ProjectElement
   Implements IHost

   Private FProject As Project
   Private FVisitor As IVisitor

   Public Sub New()

   End Sub

   Public Sub New(ByVal Project As Project)
      FProject = Project
   End Sub

   Public Sub Accept(ByVal visitor As IVisitor) _
          Implements IHost.Accept
      FVisitor = visitor
      visitor.Visit(Me)
   End Sub

   Public Property CurrentProject() As Project
      Get
         Return FProject
      End Get
      Set(ByVal Value As Project)
         FProject = Value
      End Set
   End Property

   Public Sub Iterate()
      Debug.Assert(FProject Is Nothing = False)

      Try
         Output.WriteLine("Project: " & FProject.Name)
      Catch
         Output.WriteLine("Project: <no name>")
      End Try

      If (FProject.ProjectItems Is Nothing) Then Return
      FVisitor.ProjectCount += 1

      Dim item As ProjectItem
      Dim itemElement As ItemElement = New ItemElement

      For Each item In FProject.ProjectItems
         itemElement.CurrentItem = item
         itemElement.Accept(FVisitor)
      Next
   End Sub
End Class

The ProjectElement represents an extensibility object model Project. Again, we implement IHost and call Accept to visit our host. The ProjectElement host also implements Iterate. At this level of granularity, we have more work to do. We output the project name, increment the visitor's ProjectCount, and then examine each ProjectItem using the ItemElement wrapper.

Listing 7: The ItemElement.

Imports EnvDTE
Imports System
Imports System.Diagnostics

Public Class ItemElement
   Implements IHost

   Private FProjectItem As ProjectItem
   Private FVisitor As IVisitor

   Public Sub New()

   End Sub

   Public Sub New(ByVal item As ProjectItem)
      FProjectItem = item
   End Sub

   Public Property CurrentItem() As ProjectItem
      Get
         Return FProjectItem
      End Get
      Set(ByVal Value As ProjectItem)
         FProjectItem = Value
      End Set
   End Property

   Public Sub Accept(ByVal visitor As IVisitor) _
          Implements IHost.Accept
      FVisitor = visitor
      visitor.Visit(Me)
   End Sub

   Public Sub Iterate(Optional ByVal Indent As String = "  ")
      Iterate(FProjectItem, Indent)
   End Sub

   Private Sub Iterate(ByVal item As ProjectItem, _
      Optional ByVal Indent As String = "  ")
         Try
            Output.Write(Indent & "Name: " & item.Name)
            UpdateLineCount(item)
            FVisitor.ItemCount += 1

            If (FProjectItem.ProjectItems Is Nothing = False _
               Or FProjectItem.ProjectItems.Count > 0) Then

               Dim child As ProjectItem

               For Each child In item.ProjectItems
                  Iterate(child, New String(" ", Indent.Length + 2))
               Next
            End If
         Catch e As Exception
            Debug.WriteLine(e.Message)
         End Try
   End Sub

   Private Function GetLineCount(ByVal item As ProjectItem)
      ' "{6BB5F8EE-4483-11D3-8BCF-00C04F8EC28C}"
      If (item.Kind = EnvDTE.Constants.vsProjectItemKindPhysicalFile) _
         Then
         Return LineCounter.GetLineCount(item)
      Else
         Return 0
      End If
   End Function

   Private Sub UpdateLineCount(ByVal item As ProjectItem)
      Try
         Dim count As Long = GetLineCount(item)
         Output.WriteLine(String.Format("({0})", count))
         FVisitor.LineCount += count
      Catch
         Output.WriteLine("(0)")
      End Try
   End Sub
End Class

ItemElement performs the most work. Again, it is a host with an Accept method and we want to iterate the ItemElement. Keep in mind that projects may have folders (sub-projects), so we have to look out for nested project items.

Iterate writes the ProjectItem name and updates the line count to include the ProjectItem contained in the ItemElement wrapper. Next, we update the visitor's ItemCount. After we have performed the information-gathering steps, we need to see whether this element has sub-elements. If the ProjectItem has sub-projects, we use recursion to examine those elements.

Two more methods are important to note: GetLineCount and UpdateLineCount. UpdateLineCount calls GetLineCount to get the actual number of lines, output that result, and add the count to the visitor's tally. GetLineCount uses the LineCounter class I implemented, which I cover in the next section.

Prime Programming Proficiency, Part 3: Lines-of-Code Counter

Implementing the Line Counter

The LineCounter is a completely separate class. As a result, I can stub it out with an imperfect implementation and improve it at a later date or outsource it to someone who may specialize in the counting of source lines of code. Because the LineCounter (see Listing 8) does not know about the visitor and hosts, another developer will not need my entire implementation to do his or her job, implementing the LineCounter.

Listing 8: The LineCounter class sub-divides lines of a separate utility.

Imports EnvDTE
Imports System
Imports System.IO
Imports System.Diagnostics
Imports System.Text.RegularExpressions

Public Module LineCounter

   Public Function GetLineCount(ByVal Item As ProjectItem)
      If (Not IsValidItem(Item)) Then Return 0

      Return CountLines(Item)
   End Function

   Private Function IsValidItem(ByVal item As ProjectItem) As Boolean
      Return IsValidItem(item.Name)
   End Function

   Private Function IsValidItem(ByVal FileName As String) As Boolean
      Return Regex.IsMatch(FileName, "^\w+.cs$") Or _
      Regex.IsMatch(FileName, "^\w+.ascx.cs$") _
         Or Regex.IsMatch(FileName, "^\w+.aspx.cs$") Or _
         Regex.IsMatch(FileName, "^\w+.aspx$") _
         Or Regex.IsMatch(FileName, "^\w+.ascx")
   End Function

   Private Function CountLines(ByVal item As ProjectItem) As Long
      Try
         Return DoCountLines(item)
      Catch ex As Exception
         Return DoManualCount(item.FileNames(1))
      End Try
   End Function

   Private Function DoManualCount(ByVal FileName As String) As Long
      Dim reader As TextReader = New StreamReader(FileName)
      Dim all As String = reader.ReadToEnd()

      Return Regex.Matches(all, vbCrLf).Count()
   End Function



   Private Function DoCountLines(ByVal item As ProjectItem) As Long
      Debug.Assert(IsValidItem(item))

      Dim Count As Long = 0

      Open(item)
      Try
         Dim s As TextSelection = item.Document.Selection()
         StoreOffset(s)

         s.EndOfDocument()
         Count = s.ActivePoint.Line()

         RestoreOffset(s)

      Finally
         Close(item)
      End Try

      Return Count
   End Function

   Private WasOpen As Boolean = False
   Private Current As Long = 0
   Private Sub Open(ByVal item As ProjectItem)
      WasOpen = item.IsOpen
      If (Not WasOpen) Then item.Open()
   End Sub

   Private Sub Close(ByVal item As ProjectItem)
      If (Not WasOpen) Then
         item.Document.Close()
      End If
   End Sub

   Private Sub StoreOffset(ByVal selection As TextSelection)
      Current = selection.ActivePoint.Line
   End Sub

   Private Sub RestoreOffset(ByVal selection As TextSelection)
      If (WasOpen) Then
         selection.MoveToLineAndOffset(Current, 0)
      End If
   End Sub
End Module

Oddly enough, the extensibility object model does not seem to have a straightforward property for getting the number of lines in a project item. (Perhaps a means of doing this exits, but I just can't find it.)

Two methods in this class try to count the number of lines: DoCountLines and DoManualCount. DoCountLines uses the ProjectItem passed to the constructor, opens the file represented by ProjectItem, stores the current position in the file, moves the cursor to the end of the document, and then asks what the active line is. Finally, it restores the offset and closes the ProjectItem. Both Open and Close take into account whether the ProjectItem was already opened or not and close only ProjectItems that the class opened.

If an exception occurs in DoCountLines, the caller, CountLines, catches the exception and calls DoManualCount. DoManualCount opens the ProjectItem and attempts to count carriage return and line feed pairs. DoManualCount is a lot slower then DoCountLines, but together they seem to form a resilient pair.

Note: Kevin McFarlane sent me an e-mail that mentions Oz Solomnovich's Project Line Counter add-in. You should be able to find the source here: http://wndtabs.com/plc/. I haven't looked at the source, but the GUI looks good.

Prime Programming Proficiency, Part 3: Lines-of-Code Counter

Implementing the Output Window

The last piece is the Output class. The extensibility object model has an OutputWindowPane. Again, I wrapped the existing OutputWinowPane to add some convenience methods for my specific purposes. Listing 9 shows the code.

Listing 9: A Wrapper for the OutputWindowPane.

Imports EnvDTE
Imports System.Diagnostics

Public Class Output
   Private Shared FOutputWindow As OutputWindowPane

   Shared Sub New()
      FOutputWindow = GetOutputWindowPane("Project Utility")
   End Sub

   Public Shared ReadOnly Property Output() As OutputWindowPane
      Get
         Return FOutputWindow
      End Get
   End Property

   Public Shared Sub Clear()
      FOutputWindow.Clear()
   End Sub

   Public Shared Sub Write(ByVal Text As String)
      FOutputWindow.OutputString(Text)
   End Sub

   Public Shared Sub WriteLine(ByVal Text As String)
      FOutputWindow.OutputString(Text & vbCrLf)
   End Sub

   Shared Function GetOutputWindowPane(ByVal Name As String, _
      Optional ByVal show As Boolean = True) As OutputWindowPane
         Dim win As Window = _
             DTE.Windows.Item(EnvDTE.Constants.vsWindowKindOutput)
         If show Then win.Visible = True
         Dim ow As OutputWindow = win.Object
         Dim owpane As OutputWindowPane
         Try
            owpane = ow.OutputWindowPanes.Item(Name)
         Catch e As System.Exception
            owpane = ow.OutputWindowPanes.Add(Name)
         End Try
         owpane.Activate()
         Return owpane
   End Function

End Class

I borrowed GetOutputWindowPane from the Samples.Utilities module that ships with VS.NET. This window is the Output window in VS.NET, and you can supply your own pane with a suitable title. In the example, we name it Project Utility. The rest of the wrapper methods orchestrate clearing or sending text to the window pane.

Build Your Skills Base

Programming requires a huge amount of knowledge. We learn about object models, grammars, libraries and third-party tools, patterns, refactoring, algorithms, threading, database design, testing tools, delegates, events, XML, stylesheets, source control tools, and much more. It is easy to forget how much the average programmer has to know to create even a "Hello World" application.

I hope this three-part series helps you see how many of these skills are tied together to create a whole. Still, all of these skills may only make one a competent programmer. The kind of talent that creates a thing of beauty and artistry is rare indeed and very difficult to attain.

(Eventually, I will get the source for this example posted on my Web site at http://www.softconcepts.com.)

Biography

Paul Kimmel is the VB Today columnist, has written several books on .NET programming, and is a software architect. You may contact him at pkimmel@softconcepts.com if you need assistance or are interested in joining the Lansing Area .NET Users Group (glugnet.org).

Copyright © 2004 by Paul Kimmel. All Rights Reserved.



Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds