Searching and Changing Code With CodeDomVisitor

Updated on 2019-12-06

Implementing a visitor pattern over the CodeDOM

Introduction

If you've ever written code generation tools in .NET, you've likely encountered the CodeDOM - an abstract syntax tree used to represent generic code constructs like conditional statements, expressions, iteration statements, methods, properties, fields, etc. These code constructs can be rendered in any target language.

Sometimes, you will have to touch up the tree, either because you didn't have enough information during parsing it (like with my Slang Parser) or because you got it back from a 3rd party, like Microsoft's XML serialization API.

Slang Parser

This gets really tough really fast without the proper tool. Enter CodeDomVisitor. This object implements a visitor pattern over a code tree, allowing you to modify and search it with relative ease.

visitor pattern

Background

Vistors are objects that allow you to traverse through a 3rd party object model, touching all of its objects in turn, reporting them one by one, usually using recursive descent to walk through the tree. Often, they're implemented hard coded, simply by writing all the individual calls by hand, like this one, or they can use reflection. Reflection is slower, so I went the hard coded route, despite the extra work.

Using the Code

Using the code to search is super simple, so we're going to be doing search and replace to keep things interesting. Visting the demo code, example 1, we have a CodeDOM representing this code:

internal class Program {

    public static void Main(string[] args) {
        System.Console.WriteLine("Hello World!");
    }
}

For our example, the above is represented in a CodeTypeDeclaration object called cls. Below, we're going to do a simple replace, replacing the string "Hello World!" with another string:

// replace "Hello World!" with "Goodbye Cruel World!"
CodeDomVisitor.Visit(cls, (ctx) => {
    var cp = ctx.Target as CodePrimitiveExpression;
    if(null!=cp)
    {
        if(0==string.Compare("Hello World!",cp.Value as string))
        {
            // update the field
            cp.Value = "Goodbye Cruel World!";
        }
    }
});

Your little anonymous method gets called once for every object, with a context argument we've called ctx above that contains the root of the visit (ctx.Root), which contains the object initially passed to Visit(), the parent item (ctx.Parent), the member on the parent where the item came from (ctx.Member), and the target item itself (ctx.Target). It also contains a Targets property which lets you get or set the type of targets being visited. Be careful here, as if you're only visiting members for example, the visitor will never get to many parts of the tree, so choose your targets carefully. The final property it has is Cancel, which you should set to true after doing any significant modifications to the tree - see the included comments in the demo. Canceling immediately halts the visit and reports no more items.

The above is pretty simple. We just visit, looking for a CodePrimitiveExpression with our target value "Hello World!" which we then change. Doing this has not changed the path of the visit at all so we don't have to cancel visitation, although we could simply because we already found our result. Since we didn't, this will change every target it finds, not just the first one. Canceling would obviously perform more efficiently. We'll get into cancelation in the second example below:

CodeDomVisitor.Visit(cls, (ctx) => {
    var cp = ctx.Target as CodePrimitiveExpression;
    if (null != cp)
    {
        if (0 == string.Compare("Hello World!", cp.Value as string))
        {
            var newObj = new CodeArrayIndexerExpression(
                        new CodeArgumentReferenceExpression("args"),
                        new CodePrimitiveExpression(0));
            // use reflection to change the parent object.
            // since we're changing the parent, we should
            // cancel visiting because what we're visiting
            // is based on a path that no longer is in the
            // tree once this is finished. It's all orphaned.
            // ctx.Parent contains the parent object of this
            // visit and ctx.Member contains the member that
            // was used. Sometimes this can be a collection.
            // We'll want to handle that, replacing the old
            // item in the collection with the new item.
            // Members from the CodeDOM should always return
            // the first member named when reflected so this
            // is straightforward, if ugly:
            var member = ctx.Parent.GetType().GetMember(ctx.Member)[0] as PropertyInfo;
            if(typeof(System.Collections.IList).IsAssignableFrom(member.PropertyType))
            {
                // this is a collection
                var l = member.GetValue(ctx.Parent) as System.Collections.IList;
                var i = l.IndexOf(ctx.Target);
                l[i] = newObj;
            }
            else // scalar value - we just set here
                 // (not used in the demo, provided for completeness)
                member.SetValue(
                    ctx.Parent,
                    newObj);
            ctx.Cancel = true;
        }
    }
});

This is a bit more involved, because we're trying to update the parent node of what we're visiting, replacing our own node with something else. First, remember to cancel whenever you do that. Sometimes, I run Visit() in a loop with while(more), and before I cancel, I set more to true, revisiting with a "fresh" tree. That pattern should be kept in mind if you'll be doing multiple replaces. The other complication here is the reflection, but that's a different topic. Basically, what we're doing here is looking for the property indicated by Member, checking if it's a collection, and if it is, we simply replace the old member with the new one. If it isn't a collection, we just set the property to our new value.

And there you have it. There's not much else to explore here, other than the implementation, which is ugly, but straightforward. It simply visits all properties in turn.

History

  • 6th December, 2019 - Initial submission