StyleCop with team-shared rules and auto-update

The post covers how to implement StyleCop analyzers with automatically updating team-shared rules and settings. The described approach works both in Visual Studio and Rider and on any CI.

The main idea is to have code analyzers and their settings in every project. They should be added automatically whenever a new project is created. When a version of analyzers is updated or settings are changed, it should be also reflected in every project automatically.

The ideal solution to fulfill all requirements is to create a NuGet package that installs StyleCop analyzers into projects and puts their settings into the solution directory. This way code analysis happens during the development and on CI during the build.

The next step would be to solve how to distribute the package and have it in every solution.

And the last, make sure that the result of code analysis is shown as errors on CI, and as warnings - on developers' machines. So we don't slow down the development process, but still, guarantee a clean code in repositories.

1. Creating NuGet package with StyleCop analyzers

Let's create a usual .nuspec file that describes what analyzer packages we'd like to install into the target project and what settings to copy when our NuGet package is installed.

<?xml version="1.0"?>
<package>
    <metadata>
        <id>SharedCodeAnalyzers</id>
        <version>1.0.0</version>
        <description>Team-shared code analyzers and settings.</description>
        <authors>https://kmyr.pro</authors>
        <dependencies>
            <group targetFramework="netcoreapp3.1">
                <dependency id="StyleCop.Analyzers" version="1.1.118" />
            </group>
        </dependencies>
    </metadata>
</package>

The project contains the only dependency on the StyleCop.Analyzers package. These analyzers will appear in the target project where our NuGet package is installed.

The first thing is done. Now we need to provide a team-shared configuration of the analysis. It can be done in two places: .ruleset file that specifies what rules are enabled, and the stylecop.json that configures exactly how StyleCop rules behave.

The first one must be added to each project using the instruction:

<PropertyGroup>
    <CodeAnalysisRuleSet>SharedCodeAnalyzers.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>

The second one we need just to include as an additional project file:

<ItemGroup>
    <AdditionalFiles Include="stylecop.json" Link="stylecop.json" />
</ItemGroup>

Of course, we will not do it manually. For this task, we can utilize .props files supported by MSBuild. In the SharedCodeAnalyzers.props we put the instructions described above. And then we just need to add the file to the project.

Let's add the additional content to the SharedCodeAnalyzers.nuspec:

<?xml version="1.0"?>
<package>
    <metadata>
        <id>SharedCodeAnalyzers</id>
        <version>1.0.0</version>
        <description>Team-shared code analyzers and settings.</description>
        <authors>https://kmyr.pro</authors>
        <dependencies>
            <group targetFramework="netcoreapp3.1">
                <dependency id="StyleCop.Analyzers" version="1.1.118" />
            </group>
        </dependencies>
    </metadata>
    <files>
        <file src="stylecop.json" />
        <file src="SharedCodeAnalyzers.ruleset" />
        <file src="SharedCodeAnalyzers.props" target="build" />
    </files>
</package>

After installing the NuGet package stylecop.json and SharedCodeAnalyzers.ruleset appear in the cache folder (C:\Users\<user>\.nuget\packages by default), and SharedCodeAnalyzers.props is added automatically to the project because we specified target="build".

Now, let's describe what MSBuild should do with the configuration files:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0"
         xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <CodeAnalysisRuleSet>
            $(MSBuildThisFileDirectory)..\SharedCodeAnalyzers.ruleset
        </CodeAnalysisRuleSet>
    </PropertyGroup>
    <ItemGroup>
        <AdditionalFiles Include="$(MSBuildThisFileDirectory)..\stylecop.json"
                         Link="stylecop.json" />
    </ItemGroup>
</Project>

That's it. To summarize, we described a NuGet package SharedCodeAnalyzers using SharedCodeAnalyzers.nuspec file. It includes a dependency on StyleCop.Analyzers which will be added to the project when the package is installed.

We provided two configuration files: SharedCodeAnalyzers.ruleset that configures code analyzers and stylecop.json that configures StyleCop behavior. They do not appear in the project directory. Instead, they are placed in the package cache folder.

Additionally, we included SharedCodeAnalyzers.props file that is used automatically and contains instructions for adding configuration files to the project.

2. Installing the package with analyzers into every project

The easiest way to have our NuGet package in every project automatically is to use .props file again:

<?xml version="1.0" encoding="utf-8"?>
<Project>
    <ItemGroup>
        <PackageReference Include="SharedCodeAnalyzers" Version="*" />
    </ItemGroup>
</Project>

It should be placed in every repository near the .sln file. The effect will be that SharedCodeAnalyzers is added to every solution project automatically.

Obviously, we'd like to see always the latest version added to the project. That's why we specify Version="*" in the .props file.

However, there are two difficulties. The first one, we need to tweak the nuget to always take the latest version when * is used. The solution is to place a nuget.config near the .sln as well with the following content:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <config>
        <add key="dependencyVersion" value="Highest" />
    </config>
</configuration>

The second difficulty is that nuget restore takes only the latest locally cached version instead of checking the server. That's not what we want.

I don't have a solution for development machines rather than pressing the Update button in the IDE. In fact, it will just cache and use the new version without modifications in the .csproj.

As for CI builds, the trick is to add the --force flag:

dotnet build --force Solution.sln

or do a full cleanup before build.

3. Show code analysis warnings as errors on CI

During the development all code analysis results should be shown just as warnings. This is a default behavior expected from the configured .ruleset.

However, on CI the same warnings should be treated as errors.

Such behavior can be configured in the .csproj using switch TreatWarningsAsErrors:

<PropertyGroup>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

All we need is just to add the condition to the SharedCodeAnalyzers.props to understand do we build locally or on CI:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0"
         xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    ...
    <PropertyGroup Condition="'$(CI_BUILD)' == 'true'">
        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    </PropertyGroup>
</Project>

When environment variable CI_BUILD is set, the TreatWarningsAsErrors switch is enabled.

The build on CI then looks like:

CI_BUILD=true
dotnet build --force Solution.sln

Summary

The described approach can be used to distribute any Rolsyn-based analyzers (not only StyleCop) and their settings across the team.

The required references to analyzers' packages and the settings files are conveniently packed into a single NuGet package.

If a new version of analyzers is released, or team wants to modify some rules, just update the NuGet package.

Having a .props file in the solution root makes sure that analyzers are installed and up to date.

As an additional idea for consideration: add a git pre-push hook to check that .props file is in place, and do a local build to avoid errors on CI.