Search code examples
c#msbuildstatic-code-analysis

Parsing "Static Analysis Results Interchange Format (SARIF)" in MSBuild


When running various analyzers against a project using MSBuild all failures will be output in "Static Analysis Results Interchange Format (SARIF)" format (see eg. https://github.com/sarif-standard/sarif-spec). For instance a build may yield the following

{
  "version": "0.1",
  "toolInfo": {
    "toolName": "Microsoft (R) Visual C# Compiler",
    "productVersion": "1.1.0",
    "fileVersion": "1.1.0"
  },
  "issues": [
    {
      "ruleId": "SA1401",
      "locations": [
        {
          "analysisTarget": [
            {
              "uri": "C:\\SomeFile.cs",
              "region": {
                "startLine": 708,
                "startColumn": 30,
                "endLine": 708,
                "endColumn": 36
              }
            }
          ]
        }
      ],
      "shortMessage": "Field must be private",
      "fullMessage": "A field within a C# class has an access modifier other than private.",
      "properties": {
        "severity": "Warning",
        "warningLevel": "1",
        "defaultSeverity": "Warning",
        "title": "Fields must be private",
        "category": "StyleCop.CSharp.MaintainabilityRules",
        "helpLink": "https:\/\/github.com\/DotNetAnalyzers\/StyleCopAnalyzers\/blob\/master\/documentation\/SA1401.md",
        "isEnabledByDefault": "True",
        "isSuppressedInSource": "True"
      }
    }
  ]
}

Now I would like to be able to parse the data above in the simplest way possible (and break the build if any non-suppressed issues are encountered). How to go about doing this?

PS. Preferably I would also like to avoid implementing my own MSBuild tasks and installing specific software (eg. PowerShell 3.0 - ConvertFrom-Json).


Solution

  • Since there apparently is no built-in way of doing this I ended up using an inline msbuild task (https://msdn.microsoft.com/en-US/library/dd722601.aspx), defined as

    <UsingTask TaskName="ParseUnsupressedAnalysisIssues" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll" >
        <ParameterGroup>
            <Files ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
            <Result ParameterType="Microsoft.Build.Framework.ITaskItem[]" Output="true" />
        </ParameterGroup>
        <Task>
            <Reference Include="System.Runtime.Serialization" />
            <Reference Include="System.Xml" />
            <Reference Include="System.Xml.Linq" />
            <Using Namespace="System"/>
            <Using Namespace="System.Collections.Generic"/>
            <Using Namespace="System.IO"/>
            <Using Namespace="System.Linq"/>
            <Using Namespace="System.Runtime.Serialization.Json"/>
            <Using Namespace="System.Xml"/>
            <Using Namespace="System.Xml.Linq"/>
            <Code Type="Fragment" Language="cs">
                <![CDATA[
                List<TaskItem> taskItems = new List<TaskItem>();
                foreach(ITaskItem item in Files)
                {
                    try
                    {
                        string path = item.GetMetadata("FullPath");
                        using (FileStream fs = new FileStream(path, FileMode.Open))
                        using (XmlDictionaryReader reader = JsonReaderWriterFactory.CreateJsonReader(fs, XmlDictionaryReaderQuotas.Max))
                        {
                            XElement doc = XElement.Load(reader);
                            XElement issuesRoot = doc.Elements("issues").SingleOrDefault();
                            List<XElement> unsupressedIssues = issuesRoot.Elements("item").Where(e => !"True".Equals((string)e.Element("properties").Element("isSuppressedInSource"), StringComparison.Ordinal)).ToList();
                            string unsupressedIssuesString = string.Join(Environment.NewLine, unsupressedIssues);
                            if(!string.IsNullOrEmpty(unsupressedIssuesString))
                            {
                                taskItems.Add(new TaskItem(item.ItemSpec));
                                Console.WriteLine(unsupressedIssuesString);
                            }
                        }
                    }
                    catch(Exception e)
                    {
                        taskItems.Add(new TaskItem(item.ItemSpec));
                        Console.WriteLine(e.ToString());
                    }
                }
    
                Result = taskItems.ToArray();
                ]]>
            </Code>
        </Task>
    </UsingTask>
    

    which can then be invoked as

    <ParseUnsupressedAnalysisIssues Files="@(AnalyzerFiles)">
        <Output ItemName="FailedAnalyzerFiles" TaskParameter="Result" />
    </ParseUnsupressedAnalysisIssues>
    <Error Text="FxCopAll: Following assemblies had analyzer errors @(FailedAnalyzerFiles)" Condition="'@(FailedAnalyzerFiles->Count())' &gt; 0" Code="2"/>