ReSharper's Structural Search

hmemcpy edited this page Nov 18, 2012 · 7 revisions

Rather than parsing and inspecting every code file individually, Agent Mulder plugin utilizes ReSharper's Structural Search (sometimes known as Structural Search and Replace, or SSR) to look for patterns of code in the entire solution. This is both effective (since the solution structure already exists in ReSharper's caches), and makes it very easy to extend Agent Mulder to support additional containers.

Here's an overview of ReSharper's Structural Search API (based on ReSharper SDK v6.1)

Patterns and placeholders

If you ever used ReSharper's Search with Pattern dialog, you should be already familiar with how to define custom patterns to look for in your code.

Consider the following registration with Castle Windsor:

IWindsorContainer container = new WindsorContainer();
container.Register(
     Classes.FromAssemblyContaining<IFoo>().BasedOn<IFoo>());

Suppose we want to find the registration part (the arguments to container.Register() method) using ReSharper. We need to substitute the various identifiers with placeholders, so any type could be found. Go to the ReSharper menu, then Find, then Search with Pattern. In the search dialog write the following expression as is:

Classes.FromAssemblyContaining<$type$>().BasedOn<$service$>()

You'll notice the $type$ and $service$ keywords are highlighted in red - this is because the matching placeholder definitions are not yet created. Click the Add Placeholder button, the select Extract from pattern. The dialog will create two placeholders, called type and service, both will have meaning Type.

After clicking Find, you'll see the result in the popup toolwindow:

Let's look under the hood. After the Find button is clicked, the Search with Pattern dialog converts the contents into roughly the following structure:

IStructuralSearchPattern pattern = 
    new CSharpStructuralSearchPattern("Classes.FromAssemblyContaining<$service$>().BasedOn<$type$>()",
         new TypePlaceholder("service"),
         new TypePlaceholder("type"));

From there, the pattern and the placeholders are off to an adventure deep into ReSharper's internal structures, to search and present the results.

Additional common placeholder types:

  • ArgumentPlaceholder - matches method argument(s).
  • ExpressionPlaceholder - matches an arbitrary expression (optionally of specified exact or derived type)
  • IdentifierPlaceholder - matches an identifier, with optional RegEx
  • PatternPlaceholder - a placeholder for other placeholders (unused currently by Agent Mulder)

Instead of matching the entire pattern, it should be broken to several patterns, each matching a specific part of the entire expression. Consider this example with StructureMap:

var container = new Container(x => {
    x.For<IFoo>().Use<Foo>();
    });

Instead of creating an expression to match $x$.For<$service$>().Use<$type$>(), create two expression patterns:

  • $x$.For<$service$>() (where $x$ is an expression of type ConfigurationExpression)1 and
  • $for$.Use<$type$> (where $for$ is an expression of type CreatePluginFamilyExpression<...>)2

The reason for making separate patterns is due to the nature of a fluent API - chained API calls might not be in the expected order. By separating the patterns, it's possible to match just the chunks of the registration expression we care about.


1 There currently exists a bug in ReSharper, where a match will fail if there is a namespace with the same name as the expression type. For example, having a target class exist in a namespace TestApplication.StructureMap will cause ReSharper SSR to fail. As a workaround, prefix all types in the expression placeholders with global::, e.g. global::StructureMap.ConfigurationExpression.
2 Use the 3 dots (...) in the generic parameter to have ReSharper SSR match against the closed generic type.