Tuesday, September 2, 2008

Using GhostDoc in a macro to comment an entire solution

At work we've started leveraging Visual Studio Team System to essentially automate the checking of Standards and Best Practices (S&BPs). We decided that anything that could be automatically enforced could be deemed part of the S&BPs because it was cost effective. Anything else could be deemed guidance but would not be required. One of the aspects that we desired was being able to effectively use Sandcastle to generate technical documentation. I personally feel that the most valuable documentation from XML comments comes form the comments on Acceptance Tests since they define the expectations of the system. To make those XML comments required we created a shared MSBuild file that treated some of the XML compiler warnings as errors.

Playing Catch-Up
This unfortunately left us with a lot of catch-up work. We needed to fill in and correct thousands of missing comments. While it would have been the most valuable to add thoughtful defined explanations in those places it also would have been the most costly - and it wasn't in our project budget to take on that cost. As a result I adapted a Visual Studio macro with some code-DOM traversing material borrowed from TeamReview in order to leverage GhostDoc to automatically fill in missing comments and fix some comment structure for an entire Visual Studio solution. This allowed us to take on the endeavor of changing our S&BPs, being able to enforce them for the better, and allowed us to move forward without immediately paying for Technical Debt due to the change in S&BPs.


Again, GhostDoc comments are meant to get us over the hurdle of not being able to enforce a coding standard. I know as well as you that auto-generated stuff isn't very valuable, but without it we wouldn't be able to approach this endeavor.

Before you run the macro
Be sure your code compiles and that you have the entire solution checked out from source control. Otherwise you'll get lots and lots of prompts, potentially two for every file.

Below is the code. The macro is named after the Winston Wolf character in the movie Pulp Fiction.

The Code
Option Explicit Off
Imports System
Imports EnvDTE
Imports EnvDTE80
Imports EnvDTE90
Imports System.Diagnostics

Public Module TheWolf

Public Sub CleanUpSolution()
IterateFiles()
End Sub


Private Sub IterateFiles()
Dim project As EnvDTE.Project
Dim projects As EnvDTE.Projects
Dim window As Window
Dim target As Object
DTE.ExecuteCommand("View.
SolutionExplorer")
window = DTE.Windows.Item(Constants.vsWindowKindCommandWindow)
projects = DTE.Solution.Projects
If projects.Count = 0 Then
Exit Sub
End If


For Each project In projects
If (Not project.ProjectItems Is Nothing) Then
CleanUpProject(project.ProjectItems())
End If
Next
'RebuildSolution()
End Sub

Private Sub CleanUpProject(ByVal items)
Dim file As ProjectItem
For Each file In items
If file.Name.EndsWith(".cs") OrElse file.Name.EndsWith(".vb") Then
ActivateFile(file)
RemoveAndSortUsings()
FormatDocument()
DocumentCode(file)
End If

'Handle folders within a project
If Not file.ProjectItems() Is Nothing Then
If file.ProjectItems.Count > 0 Then
CleanUpProject(file.ProjectItems())
End If
End If
SaveAll()
CloseAll()
Next
End Sub

Private Sub RebuildSolution()
DTE.ExecuteCommand("Build.RebuildSolution")
End Sub

Private Sub SaveAll()
Try
DTE.ExecuteCommand("File.SaveAll")
Catch ex As Exception
End Try
End Sub

Private Sub CloseAll()
Try
DTE.ExecuteCommand("File.CloseAllButThis")
DTE.ExecuteCommand("File.Close")
Catch ex As Exception
End Try
End Sub

Private Sub ActivateFile(ByVal file As ProjectItem)
file.Open()
If (Not file.Document Is Nothing) Then
file.Document.Activate()
End If
Try
DTE.ExecuteCommand("View.ViewCode")
Catch ex As Exception
'do nothing - it's probably already viewable
End Try
End Sub

Private Sub RemoveAndSortUsings()
Try
DTE.ExecuteCommand("Edit.RemoveAndSort")
Catch ex As Exception
'does not work on VB code
End Try
End Sub

Private Sub FormatDocument()
DTE.ExecuteCommand("Edit.FormatDocument")
End Sub

Private Sub DocumentCode(ByVal file As ProjectItem)
Dim element As EnvDTE.CodeElement
Dim document As EnvDTE.TextDocument
document = CType(file.Document.Object(""), EnvDTE.TextDocument)
If (document Is Nothing) Then
Return
End If
DocumentElements(document, file.FileCodeModel.CodeElements)
End Sub

Private Sub DocumentElement(ByVal document As EnvDTE.TextDocument, ByVal element As EnvDTE.CodeElement)
Dim startPoint As EnvDTE.EditPoint
document.Selection.GotoLine(element.StartPoint.Line, True)
startPoint = document.CreateEditPoint(document.Selection.ActivePoint)
document.Selection.MoveToPoint(startPoint, True)
DTE.ExecuteCommand("Weigelt.GhostDoc.AddIn.DocumentThis")
End Sub

Private Sub DocumentElements(ByVal document As EnvDTE.TextDocument, ByVal elements As EnvDTE.CodeElements)
Try
For Each element In elements

Select Case element.Kind
Case vsCMElement.vsCMElementFunction
Dim func As EnvDTE.CodeFunction
func = CType(element, EnvDTE.CodeFunction)
If (func.Access <> vsCMAccess.vsCMAccessPrivate And func.Access <> vsCMAccess.vsCMAccessProject) Then
DocumentElement(document, element)
End If
DocumentElements(document, func.Children)
Case vsCMElement.vsCMElementProperty
Dim prop As EnvDTE.CodeProperty
prop = CType(element, EnvDTE.CodeProperty)
If (prop.Access <> vsCMAccess.vsCMAccessPrivate And prop.Access <> vsCMAccess.vsCMAccessProject) Then
DocumentElement(document, element)
End If
DocumentElements(document, prop.Children)
Case vsCMElement.vsCMElementEvent
Dim evt As EnvDTE80.CodeEvent
evt = CType(element, EnvDTE80.CodeEvent)
If (evt.Access <> vsCMAccess.vsCMAccessPrivate And evt.Access <> vsCMAccess.vsCMAccessProject) Then
DocumentElement(document, element)
End If
DocumentElements(document, evt.Children)
Case vsCMElement.vsCMElementClass
Dim cls As EnvDTE.CodeClass
cls = CType(element, EnvDTE.CodeClass)
If (cls.Access <> vsCMAccess.vsCMAccessPrivate And cls.Access <> vsCMAccess.vsCMAccessProject) Then
DocumentElement(document, element)
End If
DocumentElements(document, cls.Children)
Case vsCMElement.vsCMElementStruct
Dim strct As EnvDTE.CodeStruct
strct = CType(element, EnvDTE.CodeStruct)
If (strct.Access <> vsCMAccess.vsCMAccessPrivate And strct.Access <> vsCMAccess.vsCMAccessProject) Then
DocumentElement(document, element)
End If
DocumentElements(document, strct.Children)
Case vsCMElement.vsCMElementDelegate
Dim dlg As EnvDTE.CodeDelegate
dlg = CType(element, EnvDTE.CodeDelegate)
If (dlg.Access <> vsCMAccess.vsCMAccessPrivate And dlg.Access <> vsCMAccess.vsCMAccessProject) Then
DocumentElement(document, element)
End If
DocumentElements(document, dlg.Children)
Case vsCMElement.vsCMElementEnum
Dim enm As EnvDTE.CodeEnum
enm = CType(element, EnvDTE.CodeEnum)
If (enm.Access <> vsCMAccess.vsCMAccessPrivate And enm.Access <> vsCMAccess.vsCMAccessProject) Then
DocumentElement(document, element)
End If
DocumentElements(document, enm.Children)
Case (vsCMElement.vsCMElementVariable)
Dim var As EnvDTE.CodeVariable

var = CType(element, EnvDTE.CodeVariable)
If (var.Access <> vsCMAccess.vsCMAccessPrivate And var.Access <> vsCMAccess.vsCMAccessProject) Then
DocumentElement(document, element)
End If
Case vsCMElement.vsCMElementNamespace
Dim nmspc As EnvDTE.CodeNamespace
nmspc = CType(element, EnvDTE.CodeNamespace)
DocumentElements(document, nmspc.Children)
                Case vsCMElement.vsCMElementInterface
Dim inter As EnvDTE.CodeInterface
inter = CType(element, EnvDTE.CodeInterface)
If (inter.Access <> vsCMAccess.vsCMAccessPrivate And inter.Access <> vsCMAccess.vsCMAccessProject) Then
DocumentElement(document, element)
End If
DocumentElements(document, inter.Children)
End Select

Next
Catch ex As Exception

End Try

End Sub

End Module

13 comments:

Gabe said...

hello, I updated your code so it will work with VB too, there are just two small changes necessary.

1. in CleanUpProject update the file type selector this:
If file.Name.EndsWith(".cs") OrElse file.Name.EndsWith(".vb") Then

2. add a try catch with no handler around DTE.ExecuteCommand("Edit.RemoveAndSort") this fails in VB. So it can just be ignored. I am sure there is a better way to do this, but it works.

Thanks for the code!

Also, I am wondering why your code is not commented. :)

  said...

Gabe - I have updated the sample with your findings. Thanks for taking the time to let me know about them!

I certainly would get docked on comments in a code review for this macro. I felt like the names of methods were pretty explanatory, but you're right, I should have provided some highlights to readers with comments.

JB

Gabe said...

JB,

I thought I posted this earlier, but there was one more I found when I was testing.

3. In document Elements we need to add this code because of VB Modules:

Case vsCMElement.vsCMElementModule
Dim cls As EnvDTE.CodeClass
cls = CType(element, EnvDTE.CodeClass)
'we can't document a module ghostdoc doesn't support it
DocumentElements(document, cls.Children)


The modules themselves do not get comments, but the child methods do need them.

Additionally, I modified/added the public methods so I could do a single doc, and some extra code to confirm, cleanUpProject was just refactored because it shares code wiht CleanUpCurrentFile:

Public Sub CleanUpSolution()
If MsgBox("Are you sure you want to clean up the entire solution?", MsgBoxStyle.YesNo + MsgBoxStyle.Exclamation, "Clean Up Solution?") = MsgBoxResult.Yes Then
iterateFiles()
End If
End Sub

Public Sub CleanUpCurrentFile()
cleanupFile(DTE.ActiveDocument.ProjectItem)
End Sub

Private Sub cleanupFile(ByVal file As ProjectItem)
If file.Name.EndsWith(".cs") OrElse file.Name.EndsWith(".vb") Then
activateFile(file)
removeAndSortUsings()
formatDocument()
documentCode(file)
End If
End Sub

Private Sub cleanUpProject(ByVal items)
Dim file As ProjectItem
For Each file In items
cleanupFile(file)
'Handle folders within a project
If file.ProjectItems() IsNot Nothing Then
If file.ProjectItems.Count > 0 Then
cleanUpProject(file.ProjectItems())
End If
End If
saveAll()
closeAll()
Next
End Sub

Also I was joking about the comments I am just as guilty... or else why would I be interested in automating GhostDoc!

Thanks Again -Gabe

  said...

Ha! Sorry, I realize now that my head wasn't in the right place and I completely missed the very obvious humor in your post.

JB

rajiv said...

Hello...
This is one post thats very interesting and very very handy...

I have been trying to get this into working but it seems as if the macro is unable to iterate projects contained inside multiple folder stucture.....
I have a 3 level folder structure where the project lies in the 3 level. Is there any way that this can iterate through a large project structure.....

Gabe said...

Rajiv,

The easy answer is yes, you can do just about anything with the macro language.

It is just a matter of modifying the code that loops over the project. The best place to start is to put some breakpoints within the macro and see what you can do to iterate the folders you are talking about.

In the cleanupprojects you probably need to make sure you are getting all of the children files, or accounting for folders. At least this is my guess without looking at it.

If you figure anything out let us know. I mainly use this for doing one file at a time. I think it is great!

Gabe

Marcel Roma said...

Thank you for your very useful macro. SubMain has recently acquired GhostDoc, and some changes occured along the line. We must modify the macro to work with the current version of GhostDoc (2.5) by editing the macro and replacing DTE.ExecuteCommand("Weigelt.GhostDoc.AddIn.DocumentThis") with DTE.ExecuteCommand("Tools.SubMain.GhostDoc.DocumentThis").

ee said...

Also came across another issue. When you have a file in the solution without a code model (say a plain text file), the macro crashes. I just added sentinel if statements to the DocumentCode Sub:

'This was here already
If (document Is Nothing) Then
Return
End If
'This is new
If (file.FileCodeModel Is Nothing) Then
Return
End If
If (file.FileCodeModel.CodeElements Is Nothing) Then
Return
End If
'This was here already
DocumentElements(document, file.FileCodeModel.CodeElements)

Anonymous said...

Thanks a lot for your work. I was already messing up with macros and how to document the whole solution, tried Atomnineer with no luck, than write my own macro to document at least a file (with lots of TextSelection work)..but you solution is perfect.

I have only one big problem - after I ran the macro, after a few seconds, I get the "System call failed" exception (hresult 0x80010100 (RPC_E_SYS_CALL_FAILED). The same exception I got while running my other macro, so it is not an issue with your code, but with VS at all.. I have Visual Studio 2010..

Any one any Idea how to fix it? :)

Anonymous said...

Good work,
http://weblogs.asp.net/soever/archive/2007/02/20/enumerating-projects-in-a-visual-studio-solution.aspx?CommentPosted=true#commentmessage is an example of how enumerate all projects.

Rene Sørensen said...

i to have this
I have only one big problem - after I ran the macro, after a few seconds, I get the "System call failed" exception (hresult 0x80010100 (RPC_E_SYS_CALL_FAILED)

Dinesh said...

Hi, Great post
but I am getting error at EnvDTE80.DTE2.ExecuteCommand(String CommandName, String CommandArgs)
at myGhost.TheWolf.DocumentElement(TextDocument document, CodeElement element) in vsmacros://D%3A/Personal/R%26D/myGhost/myGhost.vsmacros/TheWolf:line 120

Chris Mead said...

Thank you. Worked nicely. I had a bit of trouble getting it started, but this was also my first V.S. macro so I'd guess there was some user error.