Enabling Resharper-CLI in Team Build

JetBrains is working on a Commandline interface that allows you to run the Resharper Code Inspections without opening up Visual Studio. Which is pretty cool! It's still in Early Access Program mode, but you can already download it and play around with it.

It's actually not that hard to integrate it into Team Build. Though there are a few challenges... First, let's go through the steps you'll need to take to integrate it...

Map the $/TeamProject/BuildProcesstemplate folders to disk and get the latest version. Now doubleclick the template you want to edit and find the "Run MSBuild for Project" activity.

Create a new "If" conditional activity underneath it:

I named it "If compatible with Resharper-CLI", since the commandline version can only work in the context of a Visual Studio Solution file at the moment. So if you're building individual projects or are calling an msbuild script directly, the Resharper-CLI can't help you out just yet.

Add the following expression to the if statement to check for the tell tale sign of a solution file. The extension will end on .sln:

String.Equals(Path.GetExtension(localProject), ".sln", StringComparison.OrdinalIgnoreCase)

Then drag a "Invoke Process" activity in the Then branch of the if statement and configure that to call the commandline tool:

I've installed the Resharper-CLI tools in a well-known location on my build agent, but if you want to do fancy registry magic to look up the right location, or grab it from source control, I leave that up to you.

Now we'll need to set the arguments to pass the solution to the process. And to tell it where to drop the output file:

I'm actually passing it the location to write the report to (/o), the location to store the cache (/caches-home) to speed things up and the solution path.

String.Format("/o:""{0}"" /caches-home:""{1}"" ""{2}""", System.IO.Path.Combine(outputDirectoryPerProject, System.IO.Path.GetFileNameWithoutExtension(localProject)) + ".resharper.xml", "d:\resharper-cli\cache", localProject)

I wired up the Standard Output and the Standard Error like I would always do (WriteBuildMessage, WriteBuildError), but that turned out not to work. If you'd do that and check in the process template you'll see that every message is written to the Error stream. I've logged a bug at Jetbrains for that. So I added some logic to fix that, it isn't perfect, but it's as good as I could get in a few clicks.

Remove the WriteBuildError from the "Handle Error Output" item, and instead add a switch. Let that switch trigger in the first 4 characters of the line being logged and depending on the value either log a message, a warning or an error.

Now go ahead, check in the .xaml file and trigger your build, Resharper warnings and errors should now be added to your build output.

You'll end up with a piece of xaml that will look exactly like this:

<If Condition="[String.Equals(Path.GetExtension(localProject), ".sln", StringComparison.OrdinalIgnoreCase)]" DisplayName="If compatible with Resharper-CLI" sap2010:WorkflowViewState.IdRef="If_37">
    <mtbwa:InvokeProcess Arguments="[String.Format("/o:""{0}"" /caches-home:""{1}"" ""{2}""", System.IO.Path.Combine(outputDirectoryPerProject, System.IO.Path.GetFileNameWithoutExtension(localProject)) + ".resharper.xml", "d:\resharper-cli\cache", localProject)]" DisplayName="Run Resharper-CLI for Project" FileName="d:\jetbrains-CLI\InspectCode.exe" sap2010:WorkflowViewState.IdRef="InvokeProcess_1" WorkingDirectory="[SourcesDirectory]">
        <ActivityAction x:TypeArguments="x:String">
            <DelegateInArgument x:TypeArguments="x:String" Name="errOutput" />
          <Switch x:TypeArguments="x:String" DisplayName="" Expression="[errOutput.Substring(0, If(errOutput.Length >= 4, 4, 0))]" sap2010:WorkflowViewState.IdRef="Switch`1_1">
              <mtbwa:WriteBuildMessage sap2010:WorkflowViewState.IdRef="WriteBuildMessage_3" Importance="[Microsoft.TeamFoundation.Build.Client.BuildMessageImportance.Low]" Message="[errOutput]" mva:VisualBasic.Settings="Assembly references and imported namespaces serialized as XML namespaces" />
            <mtbwa:WriteBuildWarning x:Key="WARN" sap2010:WorkflowViewState.IdRef="WriteBuildWarning_3" Importance="[Microsoft.TeamFoundation.Build.Client.BuildMessageImportance.High]" Message="[errOutput]" mva:VisualBasic.Settings="Assembly references and imported namespaces serialized as XML namespaces" />
            <mtbwa:WriteBuildError x:Key="ERRO" sap2010:WorkflowViewState.IdRef="WriteBuildError_2" Message="[errOutput]" />
        <ActivityAction x:TypeArguments="x:String">
            <DelegateInArgument x:TypeArguments="x:String" Name="stdOutput" />
          <mtbwa:WriteBuildMessage sap2010:WorkflowViewState.IdRef="WriteBuildMessage_2" Importance="[Microsoft.TeamFoundation.Build.Client.BuildMessageImportance.Low]" Message="[stdOutput]" mva:VisualBasic.Settings="Assembly references and imported namespaces serialized as XML namespaces" />

This will now drop a "[Solution].resharper.xml" in the build drop folder. I haven't yet parsed the file so that you can actually fail the build based on it, that'll be something for a future blog post.

And without me writing that future post, I was contacted over twitter to let me know that someone else has already done it for me:

@jessehouwing I Already solved it like this: https://t.co/fjtHmC0R4F

— Preben Huybrechts (@preben89) November 26, 2013

    public sealed class ParseResharperCLI : CodeActivity
        // Define an activity input argument of type string
        public InArgument<string> PathToXml { get; set; }
        public OutArgument<string[]> Warnings { get; set; }
        public OutArgument<string[]> Errors { get; set; }
        // If your activity returns a value, derive from CodeActivity<tresult>
        // and return the value from the Execute method.
        protected override void Execute(CodeActivityContext context)
            var document = XDocument.Load(context.GetValue(PathToXml), LoadOptions.None);
            var issueTypes = document.Descendants("IssueType")
                 .Select(x => new
                     Id = x.Attribute("Id").Value,
                     Severity = x.Attribute("Severity").Value
            var issues = document.Descendants("Issue")
                .Select(x => new Issue
                    File = x.Attribute("File").Value,
                    Message = x.Attribute("Message").Value,
                    Severity = issueTypes.First(t => t.Id == x.Attribute("TypeId").Value).Severity,
                    Line = x.Attribute("Line") != null ? x.Attribute("Line").Value : null,
            var warnings = issues
                .Where(i => i.Severity != "ERROR")
                .Select(i => i.ToString())
            var errors = issues
                .Where(i => i.Severity == "ERROR")
                .Select(i => i.ToString())
            context.SetValue<string[]>(this.Errors, errors);
            context.SetValue<string[]>(this.Warnings, warnings);
    public class Issue
        public string File { get; set; }
        public string Message { get; set; }
        public string Severity { get; set; }
        public string Line { get; set; }
        public override string ToString()
            return string.Format("{0}  File: {1}  Line: {2}  Message: {3} ", Severity, File, Line, Message);