Introduction
One of the mantras encouraged by Uncle Bob Martin in his classic book
Clean Code: a Handbook of Agile Craftsmanship is that
you should "leave the code cleaner than how you found it."
In our environment, the code base has been actively maintained for over 20 years. To ensure code conformity and highlight potential coding violations,
we use StyleCop. Developers are responsible to check code in their IDEs and team and code metrics are collected in an automated build environment.
The code base is stable and in many instances, you want to separate functional changes from re-factoring. If you reformat code alongside major functional
changes, it becomes difficult to answer the question: "What changed?".
Our current StyleCop checking is passive in the sense that you rely on the developer to do it or, look at the build statistics. With a
large code base, you want to incrementally tackle the problem as you touch the code. Based on this scenario, the ideal solution would show the developer
all "code smells" during check-in. If there are quick wins, the developer can tackle it while the functionality of the code in question is fresh in
their mind.
Building on the TortoiseSVN Hook allows you to use
Stylecop configurations to evaluate your code and show all violations during check-in.
This may be viewed as a "big brother" imposition on the developer.
The configurability of the hook gives flexibility to the developer and your particular environment. This removes the plausible deniability excuse if standards are not followed.
- The hook function in TortoiseSVN allows when to trigger the StyleCop analysis. It allows for 7 different places in the check-in life cycle to
trigger the functionality.
- The full flexibility and configurability of StyleCop can be used to customize the experience.
- You have the source code, what more do you need?
Building the Code
Replace the Program.c with the following code:
using StyleCop;
using System;
using System.Linq;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace TortoiseSVNStyleCop
{
internal class Program
{
public static void Main(string[] args)
{
int foundViolatons = 0;
string[] filePaths = File.ReadAllLines(args[0]);
string projectPath = GetRootPath(filePaths);
string settingsPath = Path.Combine
(System.Reflection.Assembly.GetExecutingAssembly().Location, @"Settings.StyleCop");
if (File.Exists(settingsPath))
{
settingsPath = null;
}
Console.Error.WriteLine("DEBUG: {0}", settingsPath);
StyleCopConsole styleCopConsole = new StyleCopConsole(settingsPath, false, null, null, true);
Configuration configuration = new Configuration(null);
CodeProject project = new CodeProject(0, projectPath, configuration);
foreach (string file in filePaths)
{
var loaded = styleCopConsole.Core.Environment.AddSourceCode(project, file, null);
}
List<violation> violations = new List<violation>();
styleCopConsole.ViolationEncountered += ((sender, arguments) => violations.Add(arguments.Violation));
List<string> output = new List<string>();
styleCopConsole.OutputGenerated += ((sender, arguments) => output.Add(arguments.Output));
styleCopConsole.Start(new[] { project }, true);
foreach (string file in filePaths)
{
List<violation> fileViolations = violations.FindAll(viol => viol.SourceCode.Path == file);
if (fileViolations.Count > 0)
{
foundViolatons = 1;
Console.Error.WriteLine("{0} - {1} violations.", fileViolations[0].SourceCode.Name, fileViolations.Count);
foreach (Violation violation in fileViolations)
{
Console.Error.WriteLine(" {0}: Line {1}-{2}", violation.Rule.CheckId, violation.Line, violation.Message);
}
}
}
Environment.Exit(foundViolatons);
}
private static string GetRootPath(string[] filePaths)
{
if (filePaths.Length > 0)
{
string[] testAgainst = filePaths[0].Split('/');
int noOfLevels = testAgainst.Length;
foreach (string filePath in filePaths)
{
string[] current = filePath.Split('/');
int level;
for (level = 0; level <= Math.Min(noOfLevels, current.Length) - 1; level++)
{
if (testAgainst[level] != current[level])
{
break;
}
}
noOfLevels = Math.Min(noOfLevels, level);
}
return (testAgainst.Take(noOfLevels).Aggregate((m, n) => m + "/" + n));
}
return string.Empty;
}
}
}
Registering the Hook
In a directory where SVN is deployed, right click and open the Settings tab. On the Hooks Scripts entry, add a new hook.
The working copy path is the root directory where this hook is active. It would be nice if this directory is passed in
when the hook is invoked. This would allow you to get the StyleCop settings to be loaded from this directory.
Depending on when or how you want to react, you can register it as a pre-commit hook. This will give you the option to stop until it has
no violations. If your model is to check in changes and then re-factor, you may elect to register it as a post update hook.
If you are still actively developing - just point it to your bin\Debug. You can add the StyleCop.Settings
to your project but make sure the
Copy To Output property is set to Copy Always to add to your deployment.
Invoking the Hook
If configuration is correct and you have StyleCop violations, you should see something like:
You will see a DEBUG entry on the screenshot. During development, you may want to see some internal variables. Simply write out to Console.Error
.
Comments
This was my first project using Visual Studio 2013 (coming from 2010) and a couple of things to consider.
- When you create the project, you have to set the Target framework (project properties) to .NET Framework 4 - it defaults to .NET Framework 4 Client
- My initial reaction to linking in StyleCop was to use NuGet to add it to my project. This will create the references to compile the code,
but running StyleCop will give you no violations. You need the rules engine linked in =>
StyleCop.CSharp.Rules
which is deployed with the normal StyleCop deployment
- On Stack Overflow, there is a
LINQ way
to write
GetRootPath()
that I could have used. Even that may be more efficient. I find it unreadable and prefer the old fashioned way where the logic is more
readable.
- It seems StyleCop does some sort of enumeration and analysis of the
projectPath
passed to it. If the working project directory
has a large number of files in it, I had to kill the process in task manager. It may have completed but it was eating up memory which is the
main reason for the GetRootPath()
function.
- This code was written to meet our corporate needs. It does what I intended it for and put it together in a couple of hours. Right now,
it seems to work for my personal use, but needs more industrial application to work out the kinks. Check the
GitHub Repository for the most recent version.
History
- 21st March, 2014: Initial version