Create custom code analysis rules in Visual Studio 2013

This blog explains how to create custom code analysis rules in Visual Studio 2013 and how to debug them in Visual Studio 2013.

Create a custom code analysis rule

Start with creating a new class library. Rename the generated ‘Class1’ to ‘BaseRule’.
Add references in your class library to:

  • FxCopSdk
    C:\Program Files (x86)\Microsoft Visual Studio 12.0\Team Tools\Static Analysis Tools\FxCop\\FxCopSdk.dll
  • Microsoft.Cci
    C:\Program Files (x86)\Microsoft Visual Studio 12.0\Team Tools\Static Analysis Tools\FxCop\\Microsoft.Cci.dll
  • Microsoft.VisualStudio.CodeAnalysis
    C:\Program Files (x86)\Microsoft Visual Studio 12.0\Team Tools\Static Analysis Tools\FxCop\\Microsoft.VisualStudio.CodeAnalysis.dll

In the BaseRule class we have to identify how the rules are named and where they could be found. To do this change the code in the BaseRule class.

using Microsoft.FxCop.Sdk;

namespace CustomCodeAnalysisRules
{
	public abstract class BaseRule : BaseIntrospectionRule
	{
		protected BaseRule(string name)
			: base(

				// The name of the rule (must match exactly to an entry
				// in the manifest XML)
				name,

				// The name of the manifest XML file, qualified with the
				// namespace and missing the extension
				typeof(BaseRule).Assembly.GetName().Name + ".Rules",

				// The assembly to find the manifest XML in
				typeof(BaseRule).Assembly)
		{

		}
	}
}

Now we have the framework for our new custom code analysis rules. In this example we are creating a custom rule that says that fields are not allowed. We are creating this rule because our team doesn’t want any fields in Data Transfer Objects (DTO).

We name the custom rule ‘NoFieldsAllowed’, so create a class with the name ‘NoFieldsAllowed’. The rule is inherit from our BaseRule so we can give the name of the rule to the base class.

using Microsoft.FxCop.Sdk;

namespace CustomCodeAnalysisRules
{
	internal sealed class NoFieldsAllowed : BaseRule
	{

		public NoFieldsAllowed()
			: base("NoFieldsAllowed")
		{

		}
	}
}

Next we have to say for which type of access modifiers the rule is made for. We can do this be overriding the ‘TargetVisibility’ property. We want to make this rule for all access modifiers so we have to use ‘TargetVisibilities.All’.

// TargetVisibilities 'All' will give you rule violations about public, internal, protected and private access modifiers.
public override TargetVisibilities TargetVisibility
{
	get
	{
		return TargetVisibilities.All;
	}
}

The setup for the rule is complete so we can write the implementation. Override the ‘Check’ method for a ‘Member’. Cast the member that you get as parameter to a ‘Field’. Check if the Field is ‘null’. If so, the check is not for this member.

public override ProblemCollection Check(Member member)
{
	Field field = member as Field;
	if (field == null)
	{
		// This rule only applies to fields.
		// Return a null ProblemCollection so no violations are reported for this member.
		return null;
	}
}

If this member is a field we have to create a ‘Problem’ and a ‘Resolution’. To create a resolution you must create a ‘Rules.xml’ file in your project.

<?xml version="1.0" encoding="utf-8" ?>
<Rules FriendlyName="My Custom Rules">
	<Rule TypeName="NoFieldsAllowed" Category="CustomRules.DTO" CheckId="CR1011">
		<Name>No Fields allowed</Name>
		<Description>Check if fields are used in a class.</Description>
		<Resolution>Field {0} is not allowed. Field must be removed from this class.</Resolution>
		<MessageLevel Certainty="100">Warning</MessageLevel>
		<FixCategories>NonBreaking</FixCategories>
		<Url />
		<Owner />
		<Email />
	</Rule>
</Rules>

You can add the following information to the ‘Resolution’:

  • Display name of the rule.
  • Rule description.
  • One or more rule resolutions.
  • The MessageLevel (severity) of the rule. This can be set to one of the following:
    • CriticalError
    • Error
    • CriticalWarning
    • Warning
    • Information
  • The certainty of the violation. This field represents the accuracy percentage of the rule. In other words this field describes the rule author’s confidence in how accurate this rule is.
  • The FixCategory of this rule. This field describes if fixing this rule would require a breaking change, ie a change that could break other assemblies referencing the one being analyzed.
  • The help url for this rule.
  • The name of the owner of this rule.
  • The support email to contact about this rule.

Now we have the information to show as a Resolution. We only have to show it to the user in Visual Studio. So expand the ‘Check’ method by creating a ‘Resolution’ and add it to the ‘Problems’ list of the base class.

public override ProblemCollection Check(Member member)
{
	Field field = member as Field;
	if (field == null)
	{
		// This rule only applies to fields.
		// Return a null ProblemCollection so no violations are reported for this member.
		return null;
	}

	Resolution resolution = GetResolution(
		  field  // Field {0} is not allowed. Field must be removed from this class.
		  );
	Problem problem = new Problem(resolution);
	Problems.Add(problem);

	// By default the Problems collection is empty so no violations will be reported
	// unless CheckFieldName found and added a problem.
	return Problems;
}

Test your Custom code analysis rule

To test your custom rule we can add some fields to your custom rule class. So add under the ‘Check’ method the following fields.

private string testField1 = "This should give an error!!!";
private const string testFieldconst1 = "This should give an error!!!";
protected string testField2 = "This should give an error!!!";
internal string testField3 = "This should give an error!!!";
public string testField4 = "This should give an error!!!";
public const string testFieldconst4 = "This should give an error!!!";

To test this we are going to change the “Start Action” of your class library. Open the properties of your class library and navigate to the ‘Debug’ tab.

Change the ‘Start Action’ to ‘Start external program’. Use as external program ‘FxCopCmd.exe’ from your Visual Studio directory. For me it is: ‘C:\Program Files (x86)\Microsoft Visual Studio 12.0\Team Tools\Static Analysis Tools\FxCop\FxCopCmd.exe’.

Use as Command line arguments: ‘/out:”results.xml” /file:”WebCommBack.CustomCodeAnalysisRules.dll” /rule:”WebCommBack.CustomCodeAnalysisRules.dll” /D:”C:\Program Files (x86)\Microsoft Visual Studio 12.0\Team Tools\Static Analysis Tools\FxCop”‘.

Use as working directory your debug folder in the bin folder of this class library.

Now run your application with Debug. You will see a command prompt showing up. This will check your fields with your rules. The outcome of the test will be written in your debug folder. There you will find a ‘results.xml’ file. If you open it in ‘Internet Explorer’ you will see the information. Of course you can open it in every xml tool.

Code Analysis test results

If your test is what you’ve expected, you can use your custom code analysis dll in your ruleset for your application. See here the outcome an application that uses the custom dll.

Code Analysis application results

How to fix the CA0053 error in Code Analysis in Visual Studio 2012

I upgraded one big Team project with a lot of different solutions (to separate stuff) to Visual Studio 2012. The custom Code Analysis gave me an CA0053 error on the build server. So I searched for the problem and came to this great blog post: How to fix the CA0053 error in Code Analysis in Visual Studio 2012. This solved my problem. I removed all the tags CodeAnalysisRuleDirectories and CodeAnalysisRuleSetDirectories. This was enough. Now Visual Studio 2012 would get the latest Code Analysis dll’s.

When queued another build, I still got the same error. This was not in all solutions so it had something to do with the solution file or with some of the projects inside it. After removing the project one at the time (after each try the build succeeded) I did a reset of the solution file so that was the same. At the end, all projects where the same and I still got the error. I then searched for a difference between solutions files.

There was the solution, the version of the file format was different and the Visual Studio version was different.

image

I think that I know what the cause of the problem is. Because we have a lot of different solution files (with overlapping projects in it), all the upgrades to Visual Studio 2012 where already done. Because they where already done, no file change was done so the format file version wasn’t migrated to the Visual Studio 2012 version. The custom ruleset in the projects where searching for the Visual Studio 2012 dll’s but the solution was build in Visual Studio 2010 on the build server because of the different format version.

Hopefully this fix will help someone. Let me know!

 

Update 11/06/2013:

We upgraded our solution to VS2013 and the Custom Code Analysis didn’t work again. So I builded the custom rules in VS2013 and used that version of the DLL. Now the clients worked again. When I started a CI build, the build failed with the CA0053 error. It seems that the build server has (in my opinion) a bug that it uses the format version of your solution file -1 to determine the Visual Studio version. Visual Studio 2013 uses the same format version as VS2012 (version 12) so you will end up with VS2012 on the build server. But because we have builded the custom rules against VS2013, the buildserver wont recognize the rules. The solution is to add the /p:VisualStudioVersion=12.0 parameter by the “MSBuild Argurments” in your CI build definition.

Enable Code Analysis for all projects in the solution

Ralph Jansen BlogI needed a trick to enable code analysis for all my projects. I know you can set the rules in the “Analyze/Configure Code Analysis for Solution” window but didn’t find a way to enable or disable the CA for all the projects in one action. So I created a macro to do this. The macro could also change your target framework if you like. That is nice when you upgrading your project from framework 3.5 to 4.

 

 

Imports System
Imports EnvDTE
Imports EnvDTE80
Imports EnvDTE90
Imports System.Diagnostics

Public Module ProjectUtilities

    Private Class ProjectGuids
        Public Const vsWindowsCSharp As String = "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"
        Public Const vsWindowsVBNET As String = "{F184B08F-C81C-45F6-A57F-5ABD9991F28F}"
        Public Const vsWindowsVisualCPP As String = "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}"
        Public Const vsWebApplication As String = "{349C5851-65DF-11DA-9384-00065B846F21}"
        Public Const vsWebSite As String = "{E24C65DC-7377-472B-9ABA-BC803B73C61A}"
        Public Const vsDistributedSystem As String = "{F135691A-BF7E-435D-8960-F99683D2D49C}"
        Public Const vsWCF As String = "{3D9AD99F-2412-4246-B90B-4EAA41C64699}"
        Public Const vsWPF As String = "{60DC8134-EBA5-43B8-BCC9-BB4BC16C2548}"
        Public Const vsVisualDatabaseTools As String = "{C252FEB5-A946-4202-B1D4-9916A0590387}"
        Public Const vsDatabase As String = "{A9ACE9BB-CECE-4E62-9AA4-C7E7C5BD2124}"
        Public Const vsDatabaseOther As String = "{4F174C21-8C12-11D0-8340-0000F80270F8}"
        Public Const vsTest As String = "{3AC096D0-A1C2-E12C-1390-A8335801FDAB}"
        Public Const vsLegacy2003SmartDeviceCSharp As String = "{20D4826A-C6FA-45DB-90F4-C717570B9F32}"
        Public Const vsLegacy2003SmartDeviceVBNET As String = "{CB4CE8C6-1BDB-4DC7-A4D3-65A1999772F8}"
        Public Const vsSmartDeviceCSharp As String = "{4D628B5B-2FBC-4AA6-8C16-197242AEB884}"
        Public Const vsSmartDeviceVBNET As String = "{68B1623D-7FB9-47D8-8664-7ECEA3297D4F}"
        Public Const vsWorkflowCSharp As String = "{14822709-B5A1-4724-98CA-57A101D1B079}"
        Public Const vsWorkflowVBNET As String = "{D59BE175-2ED0-4C54-BE3D-CDAA9F3214C8}"
        Public Const vsDeploymentMergeModule As String = "{06A35CCD-C46D-44D5-987B-CF40FF872267}"
        Public Const vsDeploymentCab As String = "{3EA9E505-35AC-4774-B492-AD1749C4943A}"
        Public Const vsDeploymentSetup As String = "{978C614F-708E-4E1A-B201-565925725DBA}"
        Public Const vsDeploymentSmartDeviceCab As String = "{AB322303-2255-48EF-A496-5904EB18DA55}"
        Public Const vsVSTA As String = "{A860303F-1F3F-4691-B57E-529FC101A107}"
        Public Const vsVSTO As String = "{BAA0C2D2-18E2-41B9-852F-F413020CAA33}"
        Public Const vsSharePointWorkflow As String = "{F8810EC1-6754-47FC-A15F-DFABD2E3FA90}"
    End Class

    ” Defines the valid target framework values.
    Enum TargetFramework
        Fx40 = 262144
        Fx35 = 196613
        Fx30 = 196608
        Fx20 = 131072
    End Enum

    ” Change the target framework for all projects in the current solution.
    Sub ChangeTargetFrameworkForAllProjects()
        Dim project As EnvDTE.Project
        Dim clientProfile As Boolean = False

        Write("——— CHANGING TARGET .NET FRAMEWORK VERSION ————-")
        Try
            If Not DTE.Solution.IsOpen Then
                Write("There is no solution open.")
            Else
                Dim targetFrameworkInput As String = InputBox("Enter the target framework version (Fx40, Fx35, Fx30, Fx20):", "Target Framework", "Fx40")
                Dim targetFramework As TargetFramework = [Enum].Parse(GetType(TargetFramework), targetFrameworkInput)

                If targetFramework = ProjectUtilities.TargetFramework.Fx35 Or targetFramework = ProjectUtilities.TargetFramework.Fx40 Then
                    Dim result As MsgBoxResult = MsgBox("The .NET Framework version chosen supports a Client Profile. Would you like to use that profile?", MsgBoxStyle.Question Or MsgBoxStyle.YesNo, "Target Framework Profile")
                    If result = MsgBoxResult.Yes Then
                        clientProfile = True
                    End If
                End If

                For Each project In DTE.Solution.Projects
                    If project.Kind <> Constants.vsProjectKindSolutionItems And project.Kind <> Constants.vsProjectKindMisc Then
                        ChangeTargetFramework(project, targetFramework, clientProfile)
                    Else
                        For Each projectItem In project.ProjectItems
                            If Not (projectItem.SubProject Is Nothing) Then
                                ChangeTargetFramework(projectItem.SubProject, targetFramework, clientProfile)
                            End If
                        Next

                    End If
                Next
            End If
        Catch ex As System.Exception
            Write(ex.Message)
        End Try
    End Sub

    ” Change the target framework for a project.
    Function ChangeTargetFramework(ByVal project As EnvDTE.Project, ByVal targetFramework As TargetFramework, ByVal clientProfile As Boolean) As Boolean
        Dim changed As Boolean = True

        If project.Kind = Constants.vsProjectKindSolutionItems Or project.Kind = Constants.vsProjectKindMisc Then
            For Each projectItem In project.ProjectItems
                If Not (projectItem.SubProject Is Nothing) Then
                    ChangeTargetFramework(projectItem.SubProject, targetFramework, clientProfile)
                End If
            Next
        Else
            Try
                If IsLegalProjectType(project) Then
                    SetTargetFramework(project, targetFramework, clientProfile)
                Else
                    Write("Skipping project: " + project.Name + " (" + project.Kind + ")")
                End If
            Catch ex As Exception
                Write(ex.Message)
                changed = False
            End Try
        End If

        Return changed
    End Function

    ” Determines if the project is a project that actually supports changing the target framework.
    Function IsLegalProjectType(ByVal proejct As EnvDTE.Project) As Boolean
        Dim legalProjectType As Boolean = True

        Select Case proejct.Kind
            Case ProjectGuids.vsDatabase
                legalProjectType = False
            Case ProjectGuids.vsDatabaseOther
                legalProjectType = False
            Case ProjectGuids.vsDeploymentCab
                legalProjectType = False
            Case ProjectGuids.vsDeploymentMergeModule
                legalProjectType = False
            Case ProjectGuids.vsDeploymentSetup
                legalProjectType = False
            Case ProjectGuids.vsDeploymentSmartDeviceCab
                legalProjectType = False
            Case ProjectGuids.vsDistributedSystem
                legalProjectType = False
            Case ProjectGuids.vsLegacy2003SmartDeviceCSharp
                legalProjectType = False
            Case ProjectGuids.vsLegacy2003SmartDeviceVBNET
                legalProjectType = False
            Case ProjectGuids.vsSharePointWorkflow
                legalProjectType = False
            Case ProjectGuids.vsSmartDeviceCSharp
                legalProjectType = True
            Case ProjectGuids.vsSmartDeviceVBNET
                legalProjectType = True
            Case ProjectGuids.vsTest
                legalProjectType = False
            Case ProjectGuids.vsVisualDatabaseTools
                legalProjectType = False
            Case ProjectGuids.vsVSTA
                legalProjectType = True
            Case ProjectGuids.vsVSTO
                legalProjectType = True
            Case ProjectGuids.vsWCF
                legalProjectType = True
            Case ProjectGuids.vsWebApplication
                legalProjectType = True
            Case ProjectGuids.vsWebSite
                legalProjectType = True
            Case ProjectGuids.vsWindowsCSharp
                legalProjectType = True
            Case ProjectGuids.vsWindowsVBNET
                legalProjectType = True
            Case ProjectGuids.vsWindowsVisualCPP
                legalProjectType = True
            Case ProjectGuids.vsWorkflowCSharp
                legalProjectType = False
            Case ProjectGuids.vsWorkflowVBNET
                legalProjectType = False
            Case ProjectGuids.vsWPF
                legalProjectType = True
            Case Else
                legalProjectType = False
        End Select
        Return legalProjectType
    End Function

    ” Sets the target framework for the project to the specified framework.
    Sub SetTargetFramework(ByVal project As EnvDTE.Project, ByVal targetFramework As TargetFramework, ByVal clientProfile As Boolean)
        Dim currentTargetFramework As TargetFramework = CType(project.Properties.Item("TargetFramework").Value, TargetFramework)
        Dim targetMoniker As String = GetTargetFrameworkMoniker(targetFramework, clientProfile)
        Dim currentMoniker As String = project.Properties.Item("TargetFrameworkMoniker").Value

        If currentMoniker <> targetMoniker Then
            Write("Changing project: " + project.Name + " from " + currentMoniker + " to " + targetMoniker + ".")
            project.Properties.Item("TargetFrameworkMoniker").Value = targetMoniker
            project.Properties.Item("TargetFramework").Value = targetFramework
        Else
            Write("Skipping project: " + project.Name + ", already at the correct target framework.")
        End If
    End Sub

    Function GetTargetFrameworkMoniker(ByVal targetFramework As TargetFramework, ByVal clientProfile As Boolean) As String
        Dim moniker As String = ".NETFramework,Version=v"
        Select Case targetFramework
            Case ProjectUtilities.TargetFramework.Fx20
                moniker += "2.0"

            Case ProjectUtilities.TargetFramework.Fx30
                moniker += "3.0"

            Case ProjectUtilities.TargetFramework.Fx35
                moniker += "3.5"

            Case ProjectUtilities.TargetFramework.Fx40
                moniker += "4.0"

        End Select

        If clientProfile Then
            moniker += ",Profile=Client"
        End If

        Return moniker
    End Function

    ” Writes a message to the output window
    Sub Write(ByVal s As String)
        Dim out As OutputWindowPane = GetOutputWindowPane("Change Target Framework", True)
        out.OutputString(s)
        out.OutputString(vbCrLf)
    End Sub

    ” Gets an instance of the output window
    Function GetOutputWindowPane(ByVal Name As String, Optional ByVal show As Boolean = True) As OutputWindowPane
        Dim win As Window = DTE.Windows.Item(EnvDTE.Constants.vsWindowKindOutput)
        If show Then win.Visible = True
        Dim ow As OutputWindow = win.Object
        Dim owpane As OutputWindowPane
        Try
            owpane = ow.OutputWindowPanes.Item(Name)
        Catch e As System.Exception
            owpane = ow.OutputWindowPanes.Add(Name)
        End Try
        owpane.Activate()
        Return owpane
    End Function

    ” Change the code analysis value for a project.
    Function ChangeCodeAnalysis(ByVal project As EnvDTE.Project, ByVal isCodeAnalysisEnabled As Boolean) As Boolean
        Dim changed As Boolean = True

        If project.Kind = Constants.vsProjectKindSolutionItems Or project.Kind = Constants.vsProjectKindMisc Then
            For Each projectItem In project.ProjectItems
                If Not (projectItem.SubProject Is Nothing) Then
                    ChangeCodeAnalysis(projectItem.SubProject, isCodeAnalysisEnabled)
                End If
            Next
        Else
            Try
                If IsLegalProjectType(project) Then
                    SetCodeAnalysis(project, isCodeAnalysisEnabled)
                Else
                    Write("Skipping project: " + project.Name + " (" + project.Kind + ")")
                End If
            Catch ex As Exception
                Write(ex.Message)
                changed = False
            End Try
        End If

        Return changed
    End Function

    ” Sets the code analysis for the project to the specified value.
    Sub SetCodeAnalysis(ByVal project As EnvDTE.Project, ByVal isCodeAnalysisEnabled As Boolean)
        Dim currentCAValue As String = project.ConfigurationManager.ActiveConfiguration.Properties.Item("RunCodeAnalysis").Value

        Dim targetCAValue As String
        If isCodeAnalysisEnabled Then
            targetCAValue = "True"
        Else
            targetCAValue = "False"
        End If

        If currentCAValue <> targetCAValue Then
            Write("Changing project: " + project.Name + " from " + currentCAValue + " to " + targetCAValue + ".")
            project.ConfigurationManager.ActiveConfiguration.Properties.Item("RunCodeAnalysis").Value = isCodeAnalysisEnabled
        Else
            Write("Skipping project: " + project.Name + ", already at the correct Code Analysis value.")
        End If
    End Sub

    Sub EnableOrDisableCodeAnalysisOnAllProjects()
        Dim project As EnvDTE.Project
        Dim clientProfile As Boolean = False

        Write("——— Enable or Disable Code Analysis on all projects ————-")
        Try
            If Not DTE.Solution.IsOpen Then
                Write("There is no solution open.")
            Else
                Dim codeAnalysisInput As String = InputBox("Enter 0 for disabling or 1 for enabling code analysis:", "Enabled", "0")

                Dim isCodeAnalysisEnabled As Boolean
                If codeAnalysisInput = 0 Then
                    isCodeAnalysisEnabled = False
                ElseIf codeAnalysisInput = 1 Then
                    isCodeAnalysisEnabled = True
                Else
                    Throw New InvalidOperationException("Wrong input. Only 0 or 1 is allowed")
                End If

                For Each project In DTE.Solution.Projects
                    If project.Kind <> Constants.vsProjectKindSolutionItems And project.Kind <> Constants.vsProjectKindMisc Then
                        ChangeCodeAnalysis(project, isCodeAnalysisEnabled)
                    Else
                        For Each projectItem In project.ProjectItems
                            If Not (projectItem.SubProject Is Nothing) Then
                                ChangeCodeAnalysis(projectItem.SubProject, isCodeAnalysisEnabled)
                            End If
                        Next

                    End If
                Next
            End If
        Catch ex As System.Exception
            Write(ex.Message)
        End Try
    End Sub
End Module