Aller au contenu

Les analyseurs Roslyn en C# : améliorer la qualité du code dès la compilation

Introduction

Écrire du code qui répond aux besoins fonctionnels constitue une première étape. Écrire un code performant, robuste, maintenable, évolutif et conforme aux bonnes pratiques du développement C# en est une autre. Dans l'écosystème .NET, cette tâche est en grande partie assurée par les analyseurs Roslyn. Ces composants examinent le code source dès la compilation afin de détecter des erreurs potentielles, des problèmes de performances, des risques de sécurité ou encore des violations des conventions d'implémentation.

Qu'est-ce qu'un analyseur Roslyn ?

Un analyseur Roslyn est un composant qui inspecte le code C# à partir de son arbre syntaxique (Syntax Tree) et de son modèle sémantique (Semantic Model). Un analyseur Roslyn peut effectuer différentes vérifications, telles que vérifier le respect des conventions de nommage, signaler du code mort, identifier des problèmes de performance, suggérer des améliorations du code, etc. Les anomalies détectées par les analyseurs sont affichées dans la fenêtre d'erreurs de Visual Studio.

Les analyseurs intégrés à .NET

Depuis plusieurs versions de .NET, de nombreux analyseurs sont intégrés aux SDK .NET et sont exécutés automatiquement lors de la compilation. Voici quelques exemples :

  • CA1822 : rendre une méthode statique si elle n'utilise pas les membres d'instance

  • CA1827 : préférer Any() à Count() > 0

  • CA2000 : libérer correctement les objets IDisposable

  • CA1860 : utiliser une propriété Count plutôt qu'une méthode LINQ lorsque cela est possible

Les analyseurs intégrés au SDK ne sont pas les seuls disponibles. Il existe également :

  • Les analyseurs Microsoft intégrés au SDK

  • Les analyseurs fournis par des packages NuGet (xUnit, ASP.NET Core, etc.)

  • Les analyseurs développés par des tiers (StyleCop.Analyzers, etc.)

  • Les analyseurs développés en interne pour imposer les règles de codage propres à une entreprise

Le respect de ces règles permet d'améliorer les performances et la fiabilité des applications. Les analyseurs sont regroupés en plusieurs familles :

Roselyn - Catégories des règles Chaque règle est identifiée par un identifiant unique, dont le préfixe indique généralement son origine. Par exemple, les règles de qualité .NET utilisent le préfixe CA, les règles de style de Visual Studio le préfixe IDE, les diagnostics du compilateur C# le préfixe CS et les recommandations liées au runtime .NET le préfixe .

Exemple d'analyseur

Voici un exemple où customers est de type IEnumerable<Customer> :

if (customers.Count() > 0)
{
    ...
}

peut déclencher une recommandation :

if (customers.Any())
{
    ...
}

Cette instruction est plus performante car le parcours de la collection customers s'interrompt dès qu'un élément est trouvé, alors que la méthode Count() parcourt l'intégralité de la collection. Il s'agit de la règle CA1827.

Les Code Fix Providers

Un analyseur peut être accompagné d'un Code Fix Provider qui permet à Visual Studio de proposer automatiquement une correction. Voici quelques exemples :

  • Ajouter le mot-cléstatic

  • Remplacer Count() par Any()

  • Simplifier une expression

  • Ajouter un await manquant

La correction proposée peut être appliquée d'un simple clic dans Visual Studio. Tous les analyseurs ne disposent pas d'un Code Fix Provider. Certains se contentent de signaler un problème sans pouvoir proposer de correction automatique.

Configurer les analyseurs

Les analyseurs .NET peuvent être activés et configurés directement dans le fichier projet (.csproj) :

<PropertyGroup>
    <AnalysisLevel>latest</AnalysisLevel>
    <EnableNETAnalyzers>true</EnableNETAnalyzers>
</PropertyGroup>
  • AnalysisLevel détermine l'ensemble des règles à appliquer. La valeur latest permet d'utiliser les recommandations les plus récentes disponibles avec le SDK installé

  • EnableNETAnalyzers active les analyseurs intégrés au SDK .NET

A noter qu'à partir des projets ciblant la version 5 de .NET, EnableNETAnalyzers est activé par défaut.

Le niveau de gravité de chaque règle peut ensuite être personnalisé dans un fichier nommé .editorconfig. Placé à la racine d'un projet, il s'applique à celui-ci. Placé à côté du fichier .sln (ou .slnx), il s'applique à l'ensemble des projets de la solution.

dotnet_diagnostic.CA1822.severity = warning
dotnet_diagnostic.CA2000.severity = error

Chaque règle peut être configurée indépendamment avec un niveau de gravité tel que suggestion, warning ou error. Cette possibilité permet notamment de transformer certaines recommandations en erreurs de compilation, afin d'imposer des règles de qualité de code au sein d'une équipe.

Exemple de fichier complet

Le fichier .editorconfig permet de centraliser les conventions de codage et la configuration des analyseurs Roslyn d'un projet. Il permet notamment de définir le niveau de sévérité des analyseurs (avec les valeurs none, silent, suggestion, warning ou error), ainsi que de nombreuses règles de style, telles que le formatage du code (indentation, espaces, retours à la ligne, etc.), les conventions de nommage ou encore les préférences d'écriture du langage C#.

Voici un exemple de fichier .editorconfig qui présente un exemple de configuration, intégrant à la fois des règles de style C# et la configuration de plusieurs analyseurs Roslyn.

########################################
# Configuration globale
########################################

# Tous les diagnostics sont des avertissements par défaut
dotnet_analyzer_diagnostic.severity = warning

########################################
# Style de code (IDE)
########################################

dotnet_diagnostic.IDE0005.severity = warning   # using inutilisé
dotnet_diagnostic.IDE0051.severity = suggestion # Membre privé inutilisé
dotnet_diagnostic.IDE0052.severity = warning    # Champ privé inutilisé
dotnet_diagnostic.IDE0058.severity = warning    # Valeur de retour ignorée
dotnet_diagnostic.IDE0059.severity = warning    # Affectation inutile
dotnet_diagnostic.IDE0060.severity = suggestion # Paramètre inutilisé
dotnet_diagnostic.IDE0079.severity = warning    # Suppression inutile
dotnet_diagnostic.IDE0090.severity = suggestion # new() simplifié
dotnet_diagnostic.IDE0290.severity = suggestion # Primary constructor

########################################
# Fiabilité
########################################

dotnet_diagnostic.CA1062.severity = warning     # Vérification des paramètres null
dotnet_diagnostic.CA1065.severity = warning     # Exceptions dans des membres inattendus
dotnet_diagnostic.CA1508.severity = warning     # Code mort

########################################
# Performances
########################################

dotnet_diagnostic.CA1822.severity = warning     # Peut être static
dotnet_diagnostic.CA1825.severity = warning     # Array.Empty<T>()
dotnet_diagnostic.CA1827.severity = warning     # Any() au lieu de Count()
dotnet_diagnostic.CA1828.severity = warning     # AnyAsync()
dotnet_diagnostic.CA1829.severity = warning     # Count plutôt que Count()
dotnet_diagnostic.CA1835.severity = suggestion  # Memory<byte>
dotnet_diagnostic.CA1848.severity = warning     # LoggerMessage
dotnet_diagnostic.CA1851.severity = warning     # Enumeration multiple
dotnet_diagnostic.CA1852.severity = warning     # Type interne scellé
dotnet_diagnostic.CA1859.severity = suggestion  # Type concret
dotnet_diagnostic.CA1860.severity = warning     # Any() plutôt que Length

########################################
# Sécurité
########################################

dotnet_diagnostic.CA2100.severity = warning     # SQL construit dynamiquement
dotnet_diagnostic.CA5350.severity = error       # Algorithme cryptographique faible
dotnet_diagnostic.CA5351.severity = error

########################################
# Globalisation
########################################

dotnet_diagnostic.CA1303.severity = suggestion
dotnet_diagnostic.CA1305.severity = warning
dotnet_diagnostic.CA1310.severity = warning

########################################
# Documentation
########################################

dotnet_diagnostic.CS1591.severity = none

########################################
# Qualité
########################################

dotnet_diagnostic.CA1008.severity = warning
dotnet_diagnostic.CA1031.severity = warning
dotnet_diagnostic.CA1510.severity = suggestion
dotnet_diagnostic.CA1511.severity = suggestion
dotnet_diagnostic.CA1512.severity = suggestion
dotnet_diagnostic.CA1513.severity = suggestion

########################################
# Async
########################################

dotnet_diagnostic.VSTHRD100.severity = warning
dotnet_diagnostic.VSTHRD103.severity = warning
dotnet_diagnostic.VSTHRD200.severity = warning