Saturday, July 19, 2008

How to use StyleCop and MSBuild, and apply to multiple projects?

Problem

It is good starting point to understand what is and how to use StyleCop from http://blogs.msdn.com/sourceanalysis/pages/source-analysis-msbuild-integration.aspx.
Unfortunately, one of our customer use ClearCase and CruiseControl.NET as the build server practicing Continuous Integration. In my opinion and from experience with dealing with large number of projects in the script file, it is best to keep .csproj alone because it is hard to debug what is going on in the main MSBuild script if the custom tasks are embedded or custom targets are imported in .csproj files.

To avoid embedding in .csproj and make build script some what easy to debug and have more control over the behavior of StyleCop this article will explore the use of

<UsingTask
    AssemblyFile="$(StyleCopRoot)Microsoft.SourceAnalysis.dll"
    TaskName="SourceAnalysisTask"/>


References used to research and learn

http://code.msdn.microsoft.com/sourceanalysis
http://blogs.msdn.com/sourceanalysis/pages/source-analysis-msbuild-integration.aspx

Assumption

1) The reader is somewhat familiar with MSBuild concept.
2) The reader is now using TFS build server and using other CI tools like
CruiseControl.Net.

About the project

Download: StyleCopDemo.zip

1) There are four batch files created at the root of the project that can be executed.

buildPass.bat - happy pass that passes StyleCop.
buildFail.bat - unhappy pass where StyleCop fails build.
buildExcludeFailFile.bat - demonstrates how to exlude files from being analyzed by StyleCop.
Clean.bat - removes all unnecessary files and directory.
StyleCopSetting.bat - for customizing StyleCop Settings.

2) Closer look at SourceAnalysisTask of Microsoft.SourceAnalysis.dll

Requirements

.Net 3.5
StyleCop

Step by step instruction

1) First install StyleCop. It IMPORTANT to enable "MSBuild files" feature during the installation since by default it is not included.



2) Unzip download sample project StyleCopDemo.zip to working directory.

3) Copy C:\Program Files\MSBuild\Microsoft\SourceAnalysis to contrib directory found at the root of StyleCopDemo directory

4) Open "Visual Studio 2008 Command Prompt"

5) Go to working directory of StyleCopDemo and execute buildpass.bat

build.demo.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--
****************************************************************************************
 
build.demo.targets
 
Build script demonstrating how to integrate StyleCop to multiple projects
****************************************************************************************
-->
<Project
  xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
  ToolsVersion="3.5"
  DefaultTargets="BuildDemoPass">
 
  <!--
  //////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////
  Set Deafult property values
 
  These properties can be overridden from msbuild command line.
  i.e. msbuild /p:AppRoot=c:\someproj\app\ build.demo.xml
  //////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////
  -->
 
  <PropertyGroup>
    <ProjectRoot Condition=" '$(ProjectRoot)' == '' ">$(MSBuildProjectDirectory)\..\</ProjectRoot>
    <BuildRoot Condition=" '$(BuildRoot)' == '' ">$(ProjectRoot)build\</BuildRoot>
    <AppRoot Condition=" '$(AppRoot)' == '' ">$(ProjectRoot)src\app\</AppRoot>
    <LibRoot Condition=" '$(LibRoot)' == '' ">$(ProjectRoot)src\lib\</LibRoot>
    <ToolRoot Condition=" '$(ToolRoot)' == '' ">$(ProjectRoot)src\tool\</ToolRoot>
    <ContribRoot>$(ProjectRoot)contrib\</ContribRoot>
    <ProjectSolutionRoot>$(ProjectRoot)solution\</ProjectSolutionRoot>
 
    <!-- StyleCop related stuff -->
    <StyleCopRoot>$(ContribRoot)SourceAnalysis\v4.2\</StyleCopRoot>
    <SourceAnalysisSettingsEditorEXE>$(StyleCopRoot)SourceAnalysisSettingsEditor.exe</SourceAnalysisSettingsEditorEXE>
    <SourceAnalysisSettingsFile>$(BuildRoot)Settings.SourceAnalysis</SourceAnalysisSettingsFile>
  </PropertyGroup>
 
  <!--
  //////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////
  Import StyleCop assembly
  //////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////
  -->
 
  <UsingTask
    AssemblyFile="$(StyleCopRoot)Microsoft.SourceAnalysis.dll"
    TaskName="SourceAnalysisTask"/>
 
  <!--
  //////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////
  Define projects here
  //////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////
  -->
 
  <ItemGroup>
    <Project Include="$(AppRoot)StyleCopPassWebDemo\StyleCopPassWebDemo.csproj">
      <ProjectType>Pass</ProjectType>
      <StyleCop>yes</StyleCop>
    </Project>
    <Project Include="$(LibRoot)StyleCopPassDemo1\StyleCopPassDemo1.csproj">
      <ProjectType>Pass</ProjectType>
      <StyleCop>yes</StyleCop>
    </Project>
    <Project Include="$(LibRoot)StyleCopPassDemo2\StyleCopPassDemo2.csproj">
      <ProjectType>Pass</ProjectType>
      <StyleCop>yes</StyleCop>
    </Project>
    <Project Include="$(ToolRoot)StyleCopFailWinServiceDemo\StyleCopFailWinServiceDemo.csproj">
      <ProjectType>Fail</ProjectType>
      <StyleCop>yes</StyleCop>
    </Project>
  </ItemGroup>
 
  <!--
  //////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////
  Define default values for SourceAnalysis-specific properties.
  //////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////
  -->
 
  <PropertyGroup>
    <SourceAnalysisForceFullAnalysis Condition="'$(SourceAnalysisForceFullAnalysis)' == ''">true</SourceAnalysisForceFullAnalysis>
    <SourceAnalysisCacheResults Condition="'$(SourceAnalysisCacheResults)' == ''">false</SourceAnalysisCacheResults>
    <SourceAnalysisTreatErrorsAsWarnings Condition="'$(SourceAnalysisTreatErrorsAsWarnings)' == ''">false</SourceAnalysisTreatErrorsAsWarnings>
    <SourceAnalysisEnabled Condition="'$(SourceAnalysisEnabled)' == ''">true</SourceAnalysisEnabled>
    <SourceAnalysisOverrideSettingsFile Condition="'$(SourceAnalysisOverrideSettingsFile)' == ''">$(SourceAnalysisSettingsFile)</SourceAnalysisOverrideSettingsFile>
  </PropertyGroup>
 
  <!--
  //////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////
  BuildDemoPass
 
    Runs StyleCop on the source code that passes build.
  //////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////
  -->
 
  <Target
    Name="BuildDemoPass">
 
    <CreateItem
      Include="%(Project.RootDir)%(Project.Directory)**\*.cs"
      Condition=" '%(Project.ProjectType)' == 'Pass' ">
 
      <Output TaskParameter="Include" ItemName="SourceAnalysisFiles" />
    </CreateItem>
 
    <SourceAnalysisTask
        ProjectFullPath="$(MSBuildProjectFile)"
        SourceFiles="@(SourceAnalysisFiles)"
        ForceFullAnalysis="$(SourceAnalysisForceFullAnalysis)"
        DefineConstants="$(DefineConstants)"
        TreatErrorsAsWarnings="$(SourceAnalysisTreatErrorsAsWarnings)"
        CacheResults="$(SourceAnalysisCacheResults)"
        OverrideSettingsFile="$(SourceAnalysisOverrideSettingsFile)" />
  </Target>
 
  <!--
  //////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////
  BuildDemoFail
 
    Runs StyleCop on the source code that passes build.
  //////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////
  -->
 
  <Target
    Name="BuildDemoFail">
 
    <CreateItem
      Include="%(Project.RootDir)%(Project.Directory)**\*.cs"
      Condition=" '%(Project.ProjectType)' == 'Fail' ">
 
      <Output TaskParameter="Include" ItemName="SourceAnalysisFiles" />
    </CreateItem>
 
    <SourceAnalysisTask
        ProjectFullPath="$(MSBuildProjectFile)"
        SourceFiles="@(SourceAnalysisFiles)"
        ForceFullAnalysis="$(SourceAnalysisForceFullAnalysis)"
        DefineConstants="$(DefineConstants)"
        TreatErrorsAsWarnings="$(SourceAnalysisTreatErrorsAsWarnings)"
        CacheResults="$(SourceAnalysisCacheResults)"
        OverrideSettingsFile="$(SourceAnalysisOverrideSettingsFile)" />
  </Target>
 
  <!--
  //////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////
  EditStyleCopSetting
 
    Runs SourceAnalysisSettingsEditor.exe in order to customize the stylecop setting.
  //////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////
  -->
  <Target
    Name="EditStyleCopSetting">
 
    <Exec Command="$(SourceAnalysisSettingsEditorEXE) $(SourceAnalysisSettingsFile)" />
  </Target>
 
  <!--
  //////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////
  CleanAll
 
    Cleans Everything
  //////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////
  -->
  <Target
    Name="CleanAll">
 
    <!-- execute Clean from solution target -->
    <CreateItem Include="$(ProjectSolutionRoot)**\*.sln">
      <Output TaskParameter="Include" ItemName="ProjectSolutions" />
    </CreateItem>
    <MSBuild
      Projects="@(ProjectSolutions)"
      Targets="Clean"
      Properties="Configuration=Debug"
      StopOnFirstFailure="false"
      ContinueOnError="true"
      Condition=" '@(ProjectSolutions)' != '' "  />
    <MSBuild
      Projects="@(ProjectSolutions)"
      Targets="Clean"
      Properties="Configuration=Release"
      StopOnFirstFailure="false"
      ContinueOnError="true"
      Condition=" '@(ProjectSolutions)' != '' "  />
 
    <!-- Remove bin and obj directories -->
    <CreateItem
      Include=
        "
          $(AppRoot)**\*.csproj;
          $(LibRoot)**\*.csproj;
          $(ToolRoot)**\*.csproj;
        ">
 
      <Output TaskParameter="Include" ItemName="ProjectFiles" />
    </CreateItem>
    <CreateItem
      Include=
        "
          %(ProjectFiles.RootDir)%(ProjectFiles.Directory)bin;
          %(ProjectFiles.RootDir)%(ProjectFiles.Directory)obj;
        ">
 
      <Output TaskParameter="Include" ItemName="UnnecessaryDirectories" />
    </CreateItem>
    <RemoveDir Directories="@(UnnecessaryDirectories)" Condition=" Exists('%(UnnecessaryDirectories.Identity)') " />
  </Target>
</Project>


There are two things I want to point out

1) UsingTask to acess SourceAnalysisTask in order to run StyleCop.

<UsingTask
    AssemblyFile="$(StyleCopRoot)Microsoft.SourceAnalysis.dll"
    TaskName="SourceAnalysisTask"/>


2) Using SourceAnalysisTask

<Target
    Name="BuildDemoPass">
 
    <CreateItem
      Include="%(Project.RootDir)%(Project.Directory)**\*.cs"
      Condition=" '%(Project.ProjectType)' == 'Pass' ">
 
      <Output TaskParameter="Include" ItemName="SourceAnalysisFiles" />
    </CreateItem>
 
    <SourceAnalysisTask
        ProjectFullPath="$(MSBuildProjectFile)"
        SourceFiles="@(SourceAnalysisFiles)"
        ForceFullAnalysis="$(SourceAnalysisForceFullAnalysis)"
        DefineConstants="$(DefineConstants)"
        TreatErrorsAsWarnings="$(SourceAnalysisTreatErrorsAsWarnings)"
        CacheResults="$(SourceAnalysisCacheResults)"
        OverrideSettingsFile="$(SourceAnalysisOverrideSettingsFile)" />
  </Target>


Notice that in Microsoft.SourceAnalysis.Targets it is gather Compile items in order to check with StyleCop

<CreateItem Include="@(Compile)" Condition="'%(Compile.ExcludeFromSourceAnalysis)'!='true'">
      <Output TaskParameter="Include" ItemName="SourceAnalysisFiles"/>
    </CreateItem>


In my case, I am building my own first by defining

<ItemGroup>
    <Project Include="$(AppRoot)StyleCopPassWebDemo\StyleCopPassWebDemo.csproj">
      <ProjectType>Pass</ProjectType>
      <StyleCop>yes</StyleCop>
    </Project>
    <Project Include="$(LibRoot)StyleCopPassDemo1\StyleCopPassDemo1.csproj">
      <ProjectType>Pass</ProjectType>
      <StyleCop>yes</StyleCop>
    </Project>
    <Project Include="$(LibRoot)StyleCopPassDemo2\StyleCopPassDemo2.csproj">
      <ProjectType>Pass</ProjectType>
      <StyleCop>yes</StyleCop>
    </Project>
    <Project Include="$(ToolRoot)StyleCopFailWinServiceDemo\StyleCopFailWinServiceDemo.csproj">
      <ProjectType>Fail</ProjectType>
      <StyleCop>yes</StyleCop>
    </Project>
  </ItemGroup>


and gathering .cs projects. Notice that meta tag are defined. These tags are useful for debugging or filtering out unwanted projects for specific behavior. For example, during CreateItem of files to be to analyzed by StyleCop I only want files that will pass the build so I use Condition=" '%(Project.ProjectType)' == 'Pass' ".

In this build script I am using Project meta tag StyleCop. This is useful for deugging or disabling projects from being analyzed by StyleCop. Change the conditon to Condition=" '%(Project.ProjectType)' == 'Pass' and '%(Project.StyleCop)' == yes'". In real build script, there are bunch of these metag tags like FxCop, UnitTest, Package, NDepend, and NCover so that specific projects are not accountable to be part of specific task. I found it to be very useful in deugging large projects and be able to maintain.

<CreateItem
      Include="%(Project.RootDir)%(Project.Directory)**\*.cs"
      Condition=" '%(Project.ProjectType)' == 'Pass' ">
 
      <Output TaskParameter="Include" ItemName="SourceAnalysisFiles" />
    </CreateItem>


For most of the projects I am only concerned about .cs files there other files that are compilerable and above CreateItem of SourceAnalysisFiles can be improved to include files that need to be checked. This type of control is what I like since I can control what project and what files can be checked.

Notice that in Microsoft.SourceAnalysis.Targets SourceAnalysisFiles are crated if the meta tag on Compile item meets Condition="'%(Compile.ExcludeFromSourceAnalysis)'!='true'". Only way I know you can do this is open .csproj file and add meta tag to Compile item but this is not maintaineable in my opinion.

So again my technique descibed in this article can over come this issue by adding Exclude list to CreateItem.

<Target
    Name="ExcludeFailFile">
 
    <CreateItem
      Include="%(Project.RootDir)%(Project.Directory)**\*.cs"
      Exclude=
        " 
          %(Project.RootDir)%(Project.Directory)**\Service1.cs;
          %(Project.RootDir)%(Project.Directory)**\Program.cs;
          %(Project.RootDir)%(Project.Directory)**\Service1.Designer.cs
        ">
 
      <Output TaskParameter="Include" ItemName="SourceAnalysisFiles" />
    </CreateItem>
 
    <SourceAnalysisTask
        ProjectFullPath="$(MSBuildProjectFile)"
        SourceFiles="@(SourceAnalysisFiles)"
        ForceFullAnalysis="$(SourceAnalysisForceFullAnalysis)"
        DefineConstants="$(DefineConstants)"
        TreatErrorsAsWarnings="$(SourceAnalysisTreatErrorsAsWarnings)"
        CacheResults="$(SourceAnalysisCacheResults)"
        OverrideSettingsFile="$(SourceAnalysisOverrideSettingsFile)" />
  </Target>


SourceAnalysisViolations.xml

Notice that after buildpass.bat or buildfail.bat, and buildExcludeFailFile.bat SourceAnalysisViolations.xml is created in build directory. This file can be picked up by CruiseControl.Net and create nice report using stylesheets. I couldn't find any stylesheets for this and I will work on it when I could get to it.

Settings.SourceAnalysis
SourceAnalysisSettingsEditor.exe

Notice in build.demo.xml there is SourceAnalysisSettingsFile property. It is customized setting for this demo. Much similar to FxCop rules you can include or exclude rules.

From command prompt type StyleCopSetting.bat to see the settings in GUI.



For StyleCopDemo project followings are disabled.


  • Dacumentation Rules - Disabled all of them

  • Ordering Rules - SA1200

  • Spacing Rules - SA1027

  • Hungarian - add underscore (_)

  • Naming Rules - SA1301

  • Readability Rules - SA1101

  • Layout Rules - SA1505, SA1508



Conclusion

StyleCop is like FxCop except it runs before compilation of the projects and there are some overlaps between what StyleCop checks vs FxCop checks. I think there are some debate over StyleCop is too rigid and such. My personal stand on this is that any type of Best Practices that can be enforced is good thing. FxCop is used on all our projects and it has helped tramendously in helping coding standards and I am very happy to see tools like StyleCop to help even more. Much like FxCop, StyleCop has settings or rules which can be disabled so rigidity of the rules can be losened up or customized for the company's needs and wants.

For future StyleCop I would like to see something similar to FxCop where files can be dropped from GUI just as FxCop GUI allows dll or exe to be droped and saved to file. This allows very clean automation and ability to control what gets checked.

1 comment:

Scott White said...

Nice entry, I just blogged on creating custom rules for StyleCop, requiring instance variables to be prefixed with an underscore.
http://scottwhite.blogspot.com/2008/11/creating-custom-stylecop-rules-in-c.html

I agree with your comments, I would like to see Microsoft make a CLI for StyleCop and it would be nice if they created a plugin for CruiseControl.Net as they have for TFS (CodePlex).