Using IronPython to Enforce Code Conventions in Visual Studio Builds

Recently I was reading a blog post by Scott Guthrie about the new syntax for HTML encoding output in ASP.NET 4, where he said the following:

This enables you to default to always using <%: %> code nuggets instead of <%= %> code blocks within your applications. If you want to be really hardcore you can even create a build rule that searches your application looking for <%= %> usages and flags any cases it finds as an error to enforce that HTML encoding always takes place.

It seemed like a fun exercise, so I decided to take up the challenge and build a simple rules engine for enforcing code conventions as part of the build. I know there already exist some tools out there for doing this, such as StyleCop, and they almost certainly do it more comprehensively than what I implemented. However, I thought this would be a good example of how easy it is to extend the build process with your own custom checks.

I’ve developed a bit of a habit lately of using IronPython whenever I would normally create a new console application. You get all the benefits of the .NET Framework, and the conveniences of a dynamic language. If you haven’t played with it at all, I would highly suggest doing so since dynamic languages are really takin off in the .NET space.

To start off, I had to decide what it was I wanted to try and enforce in my build. I decided to keep it simple, and have a list of rules that said which file types it should check, and what string it would search the files for. Again, in keeping it simple, I decided to just do a line-by-line check, so violations that wrap over a line break would not get caught. For basic checks, this is more than sufficient.

The Enforcer

First, let’s start writing the IronPython script to do the work for us.

import System
from System.IO import *

# assuming that we got one command line argument, which is the base
# directory to start processing in
base_dir = System.Environment.GetCommandLineArgs()[2]

Now we imported the namespaces we need, and grabbed the base project directory from the command line arguments. As you can see, even though Python has its own methods of accessing command line arguments, we’re going to stick with the .NET methods.

Next, we’ll establish the structure for the rules:

rules = [
        {
            "extensions": [ "aspx", "ascx", "Master" ],
            "searchTerm": "<b>",
            "type": "Warning",
            "message": "Please use 'strong' instead of 'b'."
        },
        {
            "extensions": [ "aspx", "ascx", "Master" ],
            "searchTerm": "<i>",
            "type": "Warning",
            "message": "Please use 'em' instead of 'i'."
        }
    ]

Each rule has a list of extensions it applies to, the string to look for, the type of violation it is (can be either “Warning” or “Error), and the message to display in the build error. The first two examples there will look for the old style bold and italic tags, and warn you that there are better tags to use.

Next, let’s define a function that given a file and a rule, will check for violations of the rule within that file.

def process_file(file, rule):
line_number = 0
line = None

reader = StreamReader(file)
line = reader.ReadLine()

while line != None:
    line_number += 1

    # does this line violate the rule?
    if line.ToLower().Contains(rule["searchTerm"].ToLower()):
        System.Console.WriteLine("%s (%d): %s: %s" % (file, line_number, rule["type"], rule["message"]))

    line = reader.ReadLine()

reader.Close()

You might be wondering why the only thing we do when we find a violation is to print it out. As it turns out, if a post-build step outputs something in the correct format, Visual Studio will treat it as an error or warning as appropriate. Including the line number makes it so that double clicking on the error brings you to the offending line. That’s all there is to it!

Now we just need to call that function on all the rules and corresponding files. For each extension found in the rules, we’ll create a bucket of all the rules that apply to it to help narrow down the number of file reads we need to do.

# explode the rules out to a dictionary with one key per extension
rule_buckets = {}

for rule in rules:
    for extension in rule["extensions"]:
        if not extension in rule_buckets:
            rule_buckets[extension] = [ rule ]
        else:
            rule_buckets[extension].append(rule)

# now start checking the rules, one extension at a time
for extension in rule_buckets:
    for file in Directory.GetFiles(base_dir, "*." + extension, SearchOption.AllDirectories):
        for rule in rule_buckets[extension]:
            process_file(file, rule)

Post-Build Hook

All that’s left is to plug this script into the post-build process of our project. In the properties section for your project (right click on the project in Visual Studio and click on Properties) go to the Build Events tab. Under “Post-build event command line” enter the following:

"C:\Program Files (x86)\IronPython 2.6\ipy.exe" "C:\projects\Scripts\convention-enforcer.py" "$(SolutionDir)\$(ProjectName)"

Obviously, replace the paths for IronPython and the python script with your own. For the “Run the post-build event” setting immediately afterwards, choose for it to run “On Successful build.” There is no reason to run these checks on a build that already failed for other reasons. The macros in the command will make sure that the checks are limited to files within the current project.

Testing the Rules

So now we have our rule enforcer tied into our project, but does it work? Let’s give it a test run. Earlier we defined a couple rules to check for bold and italic tags, so we’ll try those out. I created a new MVC project, and created a master page that contains this:

<b>This is some bold text</b>

I also created a view containing the following:

<i>This is some italicized text.</i>

Very uninteresting, but these are just test cases. When building the project, Visual Studio now reports warnings for both of these lines.

More Rules

As I mentioned earlier, this post was inspired by the Scott Guthrie post about a new tag for properly encoding HTML output, so naturally we should implement that rule. First, we extend the rules in the IronPython script to include:

{
    "extensions": [ "aspx", "ascx", "Master" ],
    "searchTerm": "<%=",
    "type": "Error",
    "message": "For security reasons, please use '<%:' instead of '<%='."
}

Now add this line to the view:

<%= ViewData["Data"] %>

As expected, the build now breaks with an error about using the outdated tag.

So far, all of the rules have been for frontend code files, so just for completeness let’s include a rule for a C# file. Perhaps you’re a pure old school developer who refuses to embrace all of this new fangled LINQ voodoo. You can now toss in a rule to the script that breaks the build if the System.Linq namespace is imported. Using this rule:

{
    "extensions": [ "cs" ],
    "searchTerm": "using System.Linq",
    "type": "Error",
    "message": "When I was your age, we didn't have any fancy LINQ to help us."
}

Now, though I wouldn’t recommend it, your build will break once the namespace gets imported.

And there you have it. By mixing some IronPython with the Visual Studio build process, we created a simple and extensible engine for quick convention checks on every successful build.