Qu'est-ce que Roslyn ? Lire, corriger et générer du code C# du point de vue du compilateur

· Mis à jour le: · · .NET, C#, Roslyn, Analyseur, Générateur de code source, Compilateur, Analyse statique, Génération de code, Réutilisation des actifs existants

1. Ce qu’il faut comprendre avant tout

Les occasions de vouloir traiter du code source C# sont plus nombreuses qu’on ne le pense. Par exemple, des tâches comme celles-ci.

Interdire l'usage d'une API particulière
Repérer mécaniquement les anciennes façons d'écrire du code
Collecter la liste des méthodes et des classes
Étudier les dépendances à l'échelle de tout le projet
Générer du code répétitif à la compilation
Avertir à la compilation des mauvais usages de sa propre bibliothèque
Mener en toute sécurité des remplacements ou des migrations à grande échelle

Dans ce genre de situation, on est tenté d’ouvrir simplement les fichiers *.cs et de les traiter avec une recherche de chaîne ou une expression régulière.

Mais le C# n’est pas une chaîne de caractères. Ces deux extraits se ressemblent, mais leur sens est totalement différent.

Console.WriteLine("Hello");
MyCompany.Logging.Console.WriteLine("Hello");

Le nom Console peut aussi désigner un type différent.

using Console = MyCompany.Logging.Console;

Console.WriteLine("Hello");

Vu comme une simple chaîne de caractères, tout cela ressemble à Console.WriteLine. Mais du point de vue du compilateur, on ne peut pas savoir s’il s’agit de System.Console.WriteLine ou d’un autre type sans effectuer la résolution de noms.

C’est ici qu’intervient Roslyn. Roslyn est une plateforme qui rend accessibles, sous forme d’API, les informations que possèdent les compilateurs C# et Visual Basic, afin que les applications et les outils puissent les utiliser.

En résumé, Roslyn permet de traiter le code C# de la façon suivante.

Le lire comme de la syntaxe, et non comme une chaîne de caractères
Le lire pour son sens, et non pour son apparence
Le lire comme un projet ou une solution, et non comme un fichier isolé
Produire, à partir de cette lecture, des avertissements, des propositions de correction et du code généré

Cet article dresse un panorama de Roslyn : Syntax Tree, SemanticModel, Workspace, Analyzer, Source Generator, et leurs usages concrets en environnement professionnel.

L’ensemble du code présenté dans cet article est publié sur GitHub sous la forme d’un jeu d’exemples complets, buildables et exécutables (une bibliothèque manipulant Syntax Tree / SemanticModel, un Analyzer qui avertit sur DateTime.Now, un Source Generator, une démonstration d’analyse d’une solution entière, et des tests unitaires vérifiant les faux positifs et les détections manquées).

roslyn-dotnet-compiler-platform - komurasoft-blog-samples (GitHub)

2. Qu’est-ce que Roslyn ?

Le nom officiel de Roslyn est le .NET Compiler Platform. C’est l’implémentation du compilateur C# et Visual Basic, et en même temps un ensemble d’API pour créer des outils d’analyse de code.

Traditionnellement, le compilateur était souvent traité comme une « boîte noire » de ce genre.

On y met le code source
Le compilateur le traite
Il en sort une DLL / un EXE

Normalement, les développeurs n’avaient pas accès aux informations produites en interne par le compilateur.

Mais en réalité, le compilateur ne se contente pas de convertir du texte en langage machine ou en IL : il construit, pendant la compilation, des informations comme celles-ci.

Cette chaîne est une déclaration de classe
Cet identificateur est une variable locale
Cet appel de méthode désigne telle méthode de tel type
Le type de retour de cette expression est string
Ce code contient une erreur de syntaxe
Cette référence désigne un type de l'assembly A
Ce using n'est en réalité pas utilisé

Roslyn rend ces informations accessibles aux développeurs. C’est pourquoi Roslyn n’est pas un simple compilateur, mais une plateforme de compréhension du code.

3. Que peut-on faire avec Roslyn ?

Roslyn permet principalement de faire ce qui suit.

Analyse syntaxique du C# / VB
Analyse sémantique des types et des méthodes
Récupération des informations de compilation
Analyse de projets ou de solutions entiers
Création d'Analyzer personnalisés
Création de Code Fix
Création de Source Generator
Création d'outils de refactoring
Génération de code
Transformation de code

Formulé de façon plus concrète, voici quelques usages.

Émettre un avertissement de build en cas d'usage d'une API interdite
Lister les emplacements utilisant une API obsolète
Vérifier les règles de nommage des méthodes async
Détecter les oublis de gestion d'IDisposable
Guider vers le bon usage de son propre framework
Générer à la compilation le code des DTO et du mapping
Générer du code répétitif à partir de fichiers de configuration ou d'attributs
Assister l'étude de migration de .NET Framework vers .NET

Ce qui rend Roslyn important, c’est qu’il permet d’écrire un traitement du code source C# sur la même base que le compilateur lui-même.

Lire du C# avec des expressions régulières ou un analyseur syntaxique maison atteint vite ses limites. Par exemple, il n’est pas simple de traiter correctement des éléments comme ceux-ci.

using alias
Méthodes d'extension
partial class
partial method
global using
Annotations nullable
Types génériques
Résolution de surcharge
Compilation conditionnelle
Directives de préprocesseur
Réécriture qui préserve les commentaires et les espaces

Roslyn fournit des API pour traiter tout cela conformément à la spécification du langage C#.

4. Roslyn distingue « syntaxe » et « sémantique »

Pour comprendre Roslyn, la première distinction à poser est celle-ci.

Syntaxe : comment le code est écrit
Sémantique : ce que ce code désigne

Prenons cet exemple de code.

Console.WriteLine(message);

Vu comme syntaxe, il a la forme suivante.

Instruction d'expression
  Expression d'appel
    Expression d'accès à un membre
      Identificateur Console
      Identificateur WriteLine
    Argument message

Mais cela seul ne dit rien du sens. On ne peut pas déterminer, à partir de la seule syntaxe, quel type est Console, quelle surcharge est WriteLine, ni quel est le type de message.

Pour en connaître le sens, toutes ces informations sont nécessaires.

L'état des using
Les assemblies référencées
Les définitions de type au sein du même projet
Les références vers d'autres projets
L'inférence de type
La résolution de surcharge
La version du langage
Le contexte nullable

Dans Roslyn, cette distinction se retrouve directement dans les API.

Syntax Tree     : représente la syntaxe du code
SemanticModel   : représente le sens de la syntaxe
Compilation     : représente l'ensemble des informations nécessaires à la compilation
Workspace       : gère la solution, les projets et les documents

Une fois cette distinction assimilée, Roslyn devient beaucoup plus facile à appréhender.

5. Qu’est-ce qu’un Syntax Tree ?

Le Syntax Tree est un arbre qui représente la structure syntaxique du code source. Prenons par exemple ce code.

class User
{
    public string Name { get; set; }

    public void Rename(string name)
    {
        Name = name;
    }
}

Du point de vue de Roslyn, ce code a globalement la structure suivante.

CompilationUnit
  ClassDeclaration: User
    PropertyDeclaration: Name
    MethodDeclaration: Rename
      Parameter: name
      Block
        ExpressionStatement
          AssignmentExpression

Le Syntax Tree n’est pas un simple découpage du texte en lignes : c’est une structure organisée selon les éléments syntaxiques du C# — classes, méthodes, propriétés, expressions, instructions, arguments, opérateurs, etc.

Voyons un exemple simple.

using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

var source = """
class User
{
    public string Name { get; set; }

    public void Rename(string name)
    {
        Name = name;
    }
}
""";

var tree = CSharpSyntaxTree.ParseText(source);
var root = tree.GetCompilationUnitRoot();

var methods = root
    .DescendantNodes()
    .OfType<MethodDeclarationSyntax>();

foreach (var method in methods)
{
    Console.WriteLine(method.Identifier.Text);
}

Ce code recherche les déclarations de méthode dans le code source et affiche leur nom. Dans cet exemple, on obtient Rename.

L’important est qu’on ne cherche pas la chaîne void, mais bien une « déclaration de méthode » en tant qu’élément de la syntaxe C#.

6. Node, Token et Trivia

Lorsqu’on manipule un Syntax Tree, ces trois termes reviennent constamment.

SyntaxNode
SyntaxToken
SyntaxTrivia

SyntaxNode

Un SyntaxNode est une unité syntaxique. Par exemple :

Déclaration de classe
Déclaration de méthode
Déclaration de propriété
Instruction if
Instruction for
Expression d'affectation
Expression d'appel
Expression lambda

Dans la syntaxe C#, tout élément susceptible d’avoir des enfants est un Node.

SyntaxToken

Un SyntaxToken est la plus petite unité qui compose la syntaxe. Par exemple :

Le mot-clé class
Le mot-clé public
L'identificateur User
L'identificateur Rename
{ ou }
; ou ,
Un littéral de chaîne
Un littéral numérique

Les Token sont les éléments situés aux feuilles de l’arbre syntaxique.

SyntaxTrivia

SyntaxTrivia désigne les informations qui n’entrent pas directement en jeu dans l’analyse sémantique habituelle, mais qui sont nécessaires pour reproduire fidèlement le code source. Par exemple :

Les espaces
Les retours à la ligne
Les commentaires
Les directives de préprocesseur

Grâce à ce Trivia, Roslyn peut traiter le code source avec une grande fidélité, commentaires et espaces compris.

Le Trivia est très important lors du formatage de code, du refactoring ou des réécritures mécaniques.

Si l’on ne cherche qu’à construire un AST, on pourrait croire qu’il est acceptable de jeter les commentaires. Mais dans les transformations de code en environnement professionnel, ne pas casser les commentaires ni les retours à la ligne est essentiel.

7. Le Syntax Tree est immuable

Le Syntax Tree de Roslyn est immuable. Autrement dit, au lieu de modifier directement l’arbre syntaxique obtenu, on crée un nouvel arbre intégrant les changements.

Par exemple, même pour changer le nom d’une méthode, on ne modifie pas sur place le MethodDeclarationSyntax existant.

var newMethod = oldMethod.WithIdentifier(
    SyntaxFactory.Identifier("NewName"));

On crée ainsi un nouveau nœud, comme ci-dessus.

L’immuabilité apporte plusieurs avantages.

Facile à manipuler depuis plusieurs threads
Permet de gérer en toute sécurité les instantanés en cours d'édition dans l'IDE
Facilite la production de diffs
Facilite la comparaison avant/après changement

Cela peut sembler un peu contraignant au premier abord. Mais dans un monde où plusieurs processus — IDE, build, Analyzer, Source Generator — référencent simultanément le même code, l’immuabilité devient un atout majeur.

8. Qu’est-ce que le SemanticModel ?

Le Syntax Tree seul ne renseigne que sur l’apparence du code. Pour en connaître le sens, on utilise le SemanticModel.

Prenons cet exemple de code.

Console.WriteLine("Hello");

Le Syntax Tree permet de savoir qu’il existe un identificateur Console et un identificateur WriteLine. Mais il ne permet pas de savoir s’ils désignent System.Console.WriteLine(string?) ou la méthode d’un autre type.

Le SemanticModel permet d’obtenir « le symbole auquel ce nœud syntaxique a été résolu ».

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

var source = """
using System;

class Program
{
    static void Main()
    {
        Console.WriteLine("Hello");
    }
}
""";

var tree = CSharpSyntaxTree.ParseText(source);

var compilation = CSharpCompilation.Create(
    assemblyName: "Sample",
    syntaxTrees: new[] { tree },
    references: new[]
    {
        MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
        MetadataReference.CreateFromFile(typeof(Console).Assembly.Location)
    });

var semanticModel = compilation.GetSemanticModel(tree);
var root = tree.GetCompilationUnitRoot();

var invocation = root
    .DescendantNodes()
    .OfType<InvocationExpressionSyntax>()
    .First();

var symbolInfo = semanticModel.GetSymbolInfo(invocation);
var method = (IMethodSymbol?)symbolInfo.Symbol;

Console.WriteLine(method?.ContainingType.ToDisplayString());
Console.WriteLine(method?.Name);

Cela permet d’obtenir la méthode à laquelle Console.WriteLine a réellement été résolu.

La force de Roslyn est de pouvoir s’appuyer non seulement sur la syntaxe, mais aussi sur le résultat de la résolution de noms effectuée par le compilateur.

9. Qu’est-ce qu’un Symbol ?

Dans Roslyn, les types, méthodes, propriétés, champs, paramètres, variables locales, etc. sont traités comme des Symbol.

Voici les principales interfaces.

INamedTypeSymbol  : classes, structs, interfaces, etc.
IMethodSymbol     : méthodes
IPropertySymbol   : propriétés
IFieldSymbol      : champs
IParameterSymbol  : paramètres
ILocalSymbol      : variables locales
INamespaceSymbol  : espaces de noms

Un Symbol représente le sens résolu par le compilateur, et non l’apparence dans le code source.

Par exemple, ces deux extraits ont une apparence différente.

System.Console.WriteLine("Hello");
using System;

Console.WriteLine("Hello");

Mais s’ils désignent tous deux le même System.Console.WriteLine, l’analyse sémantique de Roslyn les traite comme le même symbole de méthode.

Cette propriété rend possibles des vérifications comme celles-ci.

Cet appel correspond-il vraiment à une API que l'entreprise interdit ?
Ce type implémente-t-il une interface donnée ?
Cette méthode est-elle async ?
Cette valeur de retour est-elle nullable ?
Cet attribut est-il réellement appliqué ?
Cette classe hérite-t-elle d'une classe de base donnée ?

On obtient ainsi une analyse fondée sur le jugement du compilateur, et non sur une simple recherche de chaîne.

10. Qu’est-ce qu’une Compilation ?

Une Compilation rassemble l’ensemble des informations nécessaires pour compiler un programme C# ou Visual Basic.

Concrètement, elle contient des informations comme celles-ci.

L'ensemble des SyntaxTree
Les assemblies référencées
Les options de compilation
La version du langage
Les symboles prédéfinis
Les informations sur les types et les membres
Les informations de diagnostic

Pour lire un seul fichier en tant que syntaxe, un SyntaxTree suffit. Mais pour effectuer la résolution de types ou de références, il faut une Compilation.

Par exemple, lorsqu’on veut faire ce qui suit.

Savoir à quel type appartient la méthode appelée
Savoir si cette classe implémente IDisposable
Savoir quel est réellement le type de cet attribut
Connaître le type de retour de cette expression
Récupérer les erreurs et avertissements de compilation

Rien de tout cela ne peut être déterminé à partir de la seule syntaxe. Il faut prendre en compte les références du projet et les options de compilation.

11. Qu’est-ce qu’un Workspace ?

Pour traiter une solution ou un projet entier plutôt qu’un seul fichier, on utilise un Workspace.

Le Workspace manipule les unités suivantes.

Solution
Project
Document

Par exemple, pour charger tous les projets d’une solution et analyser tous les documents.

using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis.MSBuild;

MSBuildLocator.RegisterDefaults();

using var workspace = MSBuildWorkspace.Create();
var solution = await workspace.OpenSolutionAsync("Sample.sln");

foreach (var project in solution.Projects)
{
    Console.WriteLine(project.Name);

    foreach (var document in project.Documents)
    {
        var root = await document.GetSyntaxRootAsync();
        Console.WriteLine($"  {document.Name}: {root?.DescendantNodes().Count()} nodes");
    }
}

Un tel outil peut servir à l’étude d’une base de code existante ou à l’aide à la migration.

Par exemple, pour des usages comme ceux-ci.

Exporter en CSV la liste des appels à une API donnée
Lister les emplacements utilisant un ancien espace de noms
Construire la liste des API publiques
Étudier les dépendances entre projets
Rechercher les violations de convention de code dans une solution volumineuse
Effectuer des transformations de code mécaniques

L’Analyzer est un mécanisme qui fonctionne intégré à l’IDE et au build. À l’inverse, un outil console basé sur le Workspace convient davantage à l’investigation et aux migrations en masse.

Les deux se ressemblent, mais il est préférable de bien distinguer leurs cas d’usage.

12. Les principales façons d’utiliser Roslyn

On peut globalement distinguer quatre façons d’utiliser Roslyn.

1. L'utiliser comme une bibliothèque
2. Créer un Analyzer
3. Créer un Code Fix
4. Créer un Source Generator

Chacune répond à un objectif différent.

L’utiliser comme une bibliothèque

On appelle les API Roslyn depuis sa propre application console ou ses outils internes.

Voici les usages pour lesquels cette approche est adaptée.

Étude de la base de code
Conversion en masse
Collecte de métriques
Aide à la migration
Génération de rapports

Sous cette forme, on peut exécuter l’outil au moment de son choix. Comme il n’a pas besoin de tourner pendant la saisie dans l’IDE, un traitement relativement lourd reste acceptable.

Créer un Analyzer

L’Analyzer est un mécanisme qui analyse le code et produit des avertissements ou des erreurs.

On peut par exemple créer des règles comme celles-ci.

Utiliser DateTimeOffset.UtcNow plutôt que DateTime.Now
Les noms de méthode async doivent se terminer par Async
Ne pas appeler les API d'initialisation d'une bibliothèque dans le mauvais ordre
Ne pas utiliser un espace de noms donné dans le nouveau code
Interdire l'usage de Task.Result / Wait

L’Analyzer peut s’exécuter dans Visual Studio comme à la compilation. Il permet de détecter mécaniquement les manquements aux conventions d’équipe ou aux bonnes pratiques d’usage d’une bibliothèque, sans dépendre de la mémoire du relecteur.

Créer un Code Fix

Le Code Fix est un mécanisme qui propose une correction pour un problème détecté par un Analyzer.

On peut se représenter cela comme les corrections applicables depuis l’icône en forme d’ampoule dans Visual Studio.

Supposons par exemple qu’un Analyzer détecte ce code.

DateTime.Now

Le Code Fix peut alors proposer une correction comme celle-ci.

DateTimeOffset.UtcNow

La force du Code Fix est de ne pas se limiter à « émettre un avertissement », mais d’automatiser aussi « la façon sûre de corriger ».

Créer un Source Generator

Le Source Generator est un mécanisme qui génère du code à la compilation et l’ajoute à cette même compilation.

Il a par exemple des usages comme ceux-ci.

Générer du code répétitif à partir de classes portant un attribut
Générer des accesseurs fortement typés à partir de fichiers de configuration
Générer le code de mapping des DTO
Générer du code de sérialiseur
Générer le code de conversion entre enum et chaînes
Générer du code d'enregistrement de routage ou d'injection de dépendances

Il est parfois possible de remplacer un traitement qui collectait des informations par réflexion à l’exécution par du code généré à la compilation. Cela peut réduire le coût au démarrage et améliorer la compatibilité avec l’AOT.

13. L’Analyzer comme « revue de code automatisée »

En pratique, il est plus simple de considérer l’Analyzer comme une « revue de code automatisée ».

En revue de code, les mêmes remarques reviennent parfois systématiquement.

N'utilisez pas cette API
Ce nom de méthode ne respecte pas la convention
Ce catch avale l'exception
Cette vérification de null est inutile
Cet appel pose un problème de performance

Si un humain le signale à chaque fois, une partie de ces remarques peut être confiée à un Analyzer.

Les règles particulièrement adaptées à un Analyzer sont celles-ci.

Le bien et le mal peuvent être jugés sans ambiguïté
Il y a peu d'exceptions
La façon de corriger est bien établie
Toute l'équipe souhaite la respecter
Elle revient fréquemment en revue
Il est acceptable de bloquer le build

À l’inverse, certaines choses ne conviennent pas à un Analyzer.

Le jugement dépend fortement du contexte
Une décision de conception est nécessaire
Il y a trop d'exceptions
Les avis divergent selon les personnes
Il y a tant d'avertissements que plus personne ne les regarde

L’Analyzer est un outil puissant. Précisément parce qu’il est puissant, en abuser dégrade l’expérience de développement. Il est préférable de commencer par un petit nombre de règles importantes.

14. Commencer par les Analyzer inclus dans le SDK .NET

Avant d’écrire son propre Analyzer, il est réaliste de commencer par examiner les Analyzer déjà inclus dans le SDK .NET.

Dans les projets .NET 5 et versions ultérieures, l’analyse de code .NET est activée par défaut.

Les identifiants de diagnostic les plus courants appartiennent à ces deux familles.

CAxxxx : qualité du code, fiabilité, performance, sécurité, etc.
IDExxxx: style de code, assistance de l'IDE, etc.

La sévérité des Analyzer peut être ajustée via .editorconfig.

# Exemple : transformer un using inutilisé en avertissement
dotnet_diagnostic.IDE0005.severity = warning

# Exemple : transformer CA2000 en erreur
dotnet_diagnostic.CA2000.severity = error

On peut aussi activer ou durcir l’analyse depuis le fichier projet.

<PropertyGroup>
  <EnableNETAnalyzers>true</EnableNETAnalyzers>
  <AnalysisLevel>latest</AnalysisLevel>
  <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>

Introduire cela dans un projet existant peut au début faire apparaître une quantité importante d’avertissements. Dans ce cas, mieux vaut avancer par étapes plutôt que de tout transformer d’un coup en erreurs.

Commencer par avoir une vue d'ensemble du nombre total d'avertissements
Adopter la politique de ne pas en ajouter de nouveaux dans le nouveau code
Ne mettre en warning que les règles importantes
Ne mettre en error que les règles réellement incontournables
Réduire les violations existantes selon un plan

Il est utile de considérer un Analyzer personnalisé comme un complément, sur cette base, apportant les « règles propres à l’entreprise ».

15. Un Analyzer minimal

Un Analyzer recherche une syntaxe ou un symbole particulier et signale un Diagnostic.

Prenons l’exemple d’un Analyzer qui avertit sur l’usage de DateTime.Now.

Dans du code de production réel, il faudrait traiter soigneusement la résolution de types et les cas particuliers, mais voici l’image minimale.

using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class NoDateTimeNowAnalyzer : DiagnosticAnalyzer
{
    private static readonly DiagnosticDescriptor Rule = new(
        id: "CMP001",
        title: "Ne pas utiliser DateTime.Now directement",
        messageFormat: "Au lieu de DateTime.Now, envisagez DateTimeOffset.UtcNow ou une autre option adaptée à l'usage",
        category: "Usage",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true);

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
        => ImmutableArray.Create(Rule);

    public override void Initialize(AnalysisContext context)
    {
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();

        context.RegisterSyntaxNodeAction(
            AnalyzeMemberAccess,
            SyntaxKind.SimpleMemberAccessExpression);
    }

    private static void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context)
    {
        var memberAccess = (MemberAccessExpressionSyntax)context.Node;

        if (memberAccess.Name.Identifier.Text != "Now")
        {
            return;
        }

        var symbol = context.SemanticModel.GetSymbolInfo(memberAccess).Symbol;
        if (symbol is not IPropertySymbol propertySymbol)
        {
            return;
        }

        if (propertySymbol.Name == "Now" &&
            propertySymbol.ContainingType.ToDisplayString() == "System.DateTime")
        {
            var diagnostic = Diagnostic.Create(Rule, memberAccess.GetLocation());
            context.ReportDiagnostic(diagnostic);
        }
    }
}

Ce qui compte dans cet exemple, c’est qu’on ne se contente pas de chercher la chaîne DateTime.Now.

On utilise le SemanticModel pour vérifier que l’expression désigne réellement System.DateTime.Now.

Cela réduit fortement le risque de faux positifs sur un élément différent, comme ici.

MyCompany.DateTime.Now

Dans un Analyzer, ce déroulement — restreindre les candidats par la syntaxe, puis confirmer par l’analyse sémantique — est un schéma courant.

Rechercher rapidement les candidats via Syntax
Trancher précisément via le SemanticModel
Signaler l'emplacement et le message via un Diagnostic

16. Le Code Fix diffuse « la façon de corriger »

L’Analyzer trouve les problèmes, le Code Fix propose comment les corriger.

Par exemple, lorsque DateTime.Now est détecté, on peut proposer des corrections comme celles-ci.

Le remplacer par DateTimeOffset.UtcNow
Le remplacer par une abstraction telle que IClock.Now

Toutefois, un Code Fix doit être conçu avec prudence. Remplacer systématiquement DateTime.Now par DateTimeOffset.UtcNow n’est pas toujours correct. Afficher une heure locale et manipuler une heure destinée au stockage ou à la comparaison n’appellent pas le même type ni le même traitement du fuseau horaire.

Un Code Fix est donc adapté lorsque les conditions suivantes sont réunies.

Le sens après correction est sans ambiguïté
Les effets de bord sont limités
La correction peut être appliquée mécaniquement, en toute sécurité
Un humain peut facilement la vérifier

Par exemple, ce type de correction se prête bien à un Code Fix.

Remplacer un ancien nom d'API par le nouveau
Ajouter un using manquant
Renommer conformément à la convention de nommage
Ajouter un attribut
Supprimer un argument inutile

En revanche, pour les corrections qui exigent une décision de conception, il vaut parfois mieux se limiter à un avertissement plutôt qu’à une correction automatique.

17. Le Source Generator, c’est « la génération de code à la compilation »

Le Source Generator s’exécute à la compilation et ajoute le code C# généré à cette même compilation.

Le déroulement peut se résumer ainsi.

Lire le code source de l'utilisateur
Examiner les attributs et les définitions de type
Générer le code C# nécessaire
Ajouter le code généré à l'ensemble compilé

Voici un exemple simple de Source Generator.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;

[Generator]
public sealed class BuildInfoGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        context.RegisterPostInitializationOutput(static ctx =>
        {
            var source = """
namespace Generated;

public static class BuildInfo
{
    public static string Tool => "Roslyn Source Generator";
}
""";

            ctx.AddSource(
                "BuildInfo.g.cs",
                SourceText.From(source, Encoding.UTF8));
        });
    }
}

Un projet qui référence ce Generator peut utiliser ce type même sans avoir écrit de fichier source pour lui.

Console.WriteLine(Generated.BuildInfo.Tool);

Le Source Generator ne réécrit pas le code existant de l’utilisateur. Ce qu’il peut faire, c’est générer du code source supplémentaire et le faire participer à la compilation.

Il est donc utile de le voir ainsi.

Ce n'est pas un outil qui transforme le code existant
C'est un outil qui observe le code existant pour produire du code additionnel

Pour réécrire en masse du code existant, il faut envisager un outil de migration basé sur Roslyn ou un Code Fix, plutôt qu’un Source Generator.

18. Comment référencer un Source Generator

Lorsqu’on référence, en cours de développement, un projet Generator depuis un autre projet, le traitement diffère d’une référence de bibliothèque classique.

En effet, le générateur n’est pas une bibliothèque référencée à l’exécution, mais un élément chargé comme Analyzer à la compilation.

Dans une référence de projet, on la spécifie ainsi.

<ItemGroup>
  <ProjectReference Include="..\BuildInfoGenerator\BuildInfoGenerator.csproj"
                    OutputItemType="Analyzer"
                    ReferenceOutputAssembly="false" />
</ItemGroup>

En définissant ReferenceOutputAssembly="false", on évite que la DLL du Generator soit traitée comme une assembly référencée classique.

Lorsqu’on le distribue sous forme de package NuGet, il faut également le disposer de façon à ce qu’il soit chargé comme Analyzer / Source Generator.

Le Source Generator est pratique, mais son cycle de vie diffère de celui d’une bibliothèque classique.

Bibliothèque classique : utilisée par l'application à l'exécution
Source Generator       : utilisé par le compilateur à la compilation

Il est important de garder cette différence à l’esprit.

19. Ce à quoi le Source Generator est adapté

Le Source Generator n’est pas fait pour tout générer sans distinction.

Il convient au code suivant.

Fastidieux et propice aux erreurs lorsqu'il est écrit à la main
Déterminé mécaniquement à partir des informations en entrée
Le résultat généré reste lisible
Permet de réduire la réflexion à l'exécution
Permet une meilleure compatibilité avec l'AOT et le trimming
Permet de renforcer la sûreté de typage

Quelques exemples.

Métadonnées pour la sérialisation JSON
Code d'enregistrement d'injection de dépendances
Accesseurs de valeurs de configuration
Clients d'API
Code de conversion d'enum
Génération de types à partir de définitions SQL ou CSV
Code auxiliaire pour INotifyPropertyChanged

Cependant, si le code généré est trop complexe, il devient difficile à suivre lorsqu’un problème survient.

Lors de l’utilisation d’un Source Generator, il est utile de garder à l’esprit les points suivants.

Rendre le code généré consultable
Stabiliser les noms du code généré
Rendre le résultat de génération déterministe
Rendre les Diagnostic d'erreur faciles à comprendre
Éviter qu'un léger changement d'entrée ne produise un diff massif

Le code généré ne doit pas ressembler à de la magie. Il est essentiel de produire un code que les futurs mainteneurs pourront lire.

20. Ce à quoi le Source Generator n’est pas adapté

Certains traitements ne conviennent pas à un Source Generator.

Un traitement qui dépend d'un état à l'exécution
Un traitement nécessitant un accès réseau
Un traitement dépendant de la valeur actuelle d'un service externe
Un traitement dont le résultat change à chaque exécution
La réécriture de sources existantes
Une analyse massive de la solution entière

Comme le Generator s’exécute à la compilation, un Generator lent dégrade le temps de build et l’expérience dans l’IDE.

De plus, un Generator dépendant de l’environnement provoque des problèmes comme ceux-ci.

Il passe sur le poste du développeur mais échoue sur la CI
Il passe sur la CI mais échoue sur un autre OS
Le résultat change selon l'état du cache
Une panne réseau fait échouer le build

Il est préférable de rapprocher autant que possible le Source Generator d’un traitement pur.

Entrée : code source, AdditionalFiles, AnalyzerConfigOptions
Sortie : code C# généré, Diagnostic

Plus cette relation est claire, plus le Generator est stable.

21. Les outils d’investigation de code basés sur Roslyn

Les usages de Roslyn ne se limitent pas à l’Analyzer et au Source Generator. Utiliser Roslyn depuis ses propres outils console est également efficace en pratique.

On rencontre par exemple des besoins comme ceux-ci.

Lister les emplacements utilisant une API obsolète
Compter le nombre de classes publiques par projet
Exporter en CSV les classes portant un attribut donné
Étudier les espaces de noms dont dépend une solution volumineuse
Recenser les API dépendant de Windows avant une migration depuis .NET Framework

Dans ce genre de cas, il est parfois plus simple de construire un outil d’investigation ponctuel ou exécuté périodiquement plutôt qu’un Analyzer.

Voici, à titre d’exemple, une esquisse simple qui énumère les classes publiques d’une solution.

using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.MSBuild;

MSBuildLocator.RegisterDefaults();

using var workspace = MSBuildWorkspace.Create();
var solution = await workspace.OpenSolutionAsync(args[0]);

foreach (var project in solution.Projects)
{
    foreach (var document in project.Documents)
    {
        var root = await document.GetSyntaxRootAsync();
        if (root is null)
        {
            continue;
        }

        var classes = root.DescendantNodes()
            .OfType<ClassDeclarationSyntax>()
            .Where(c => c.Modifiers.Any(m => m.Text == "public"));

        foreach (var cls in classes)
        {
            Console.WriteLine($"{project.Name},{document.FilePath},{cls.Identifier.Text}");
        }
    }
}

Cet exemple ne regarde que la syntaxe. Pour rechercher « les classes publiques héritant d’une classe de base donnée », il faudrait utiliser le SemanticModel pour examiner la hiérarchie d’héritage des types.

Syntax si le nom ou la forme suffisent
SemanticModel s'il faut connaître le type ou la cible résolue
Workspace pour traiter un projet entier

Cette répartition constitue la base.

22. La différence avec les expressions régulières

Les expressions régulières sont pratiques, mais elles ne conviennent pas pour traiter le sens du code C#.

Prenons cet exemple de code.

// Console.WriteLine("debug");

Une recherche par expression régulière sur Console.WriteLine risque de capturer aussi le texte figurant dans un commentaire.

Il existe aussi des littéraux de chaîne comme celui-ci.

var text = "Console.WriteLine";

Ou bien l’appel peut être réparti sur plusieurs lignes.

Console
    .WriteLine("Hello");

On peut également utiliser un alias.

using C = System.Console;

C.WriteLine("Hello");

Il est difficile de traiter correctement tout cela avec des expressions régulières.

Avec Roslyn, on peut distinguer les commentaires, les littéraux de chaîne, les appels de méthode au sens syntaxique, et la méthode réellement résolue.

Bien sûr, pour une recherche simple, grep ou ripgrep peuvent suffire. Mais si les résultats servent de base à des décisions de conception ou à des corrections automatisées, l’usage de Roslyn est plus sûr.

Pour une recherche approximative, la recherche de chaîne suffit
Pour un jugement correct en tant que C#, utilisez Roslyn

23. Utiliser Roslyn pour l’étude de l’existant

Lors d’une migration de .NET Framework vers le .NET actuel, le premier besoin est « d’établir un état des lieux ». Roslyn est alors très utile.

Par exemple, des recherches comme celles-ci.

La liste des dépendances à System.Web
La liste du code supposant App.config / Web.config
Les emplacements utilisant des API spécifiques à Windows Forms / WPF
Les emplacements utilisant Remoting / BinaryFormatter
La présence ou non de références COM
La liste des P/Invoke
Les traitements d'E/S non asynchrones
Les emplacements utilisant d'anciennes API de chiffrement

Une simple recherche de chaîne peut déjà fournir des candidats. Mais avec Roslyn, on peut établir la liste à partir de résultats résolus comme types ou méthodes.

Par exemple, chercher uniquement la chaîne BinaryFormatter capture aussi les commentaires et la documentation.

En recherchant avec Roslyn les usages du type System.Runtime.Serialization.Formatters.Binary.BinaryFormatter, on obtient des candidats plus précis.

Dans un travail de migration, il n’est pas nécessaire de créer un Analyzer parfait dès le départ. Même un simple outil console d’investigation capable de produire un CSV comme celui-ci a déjà de la valeur.

Project,File,Line,Symbol,Kind
Legacy.Web,Controllers/HomeController.cs,42,System.Web.HttpContext.Current,Property
Legacy.Core,Serialization/OldStore.cs,18,System.Runtime.Serialization.Formatters.Binary.BinaryFormatter,Type

Disposer d’une telle liste facilite l’élaboration du plan de migration.

24. Roslyn pour les auteurs de bibliothèques

Roslyn n’est pas utile qu’aux développeurs d’applications : il sert aussi aux auteurs de bibliothèques. Toute bibliothèque a une manière correcte d’être utilisée.

Par exemple, des règles comme celles-ci.

Une méthode d'initialisation doit être appelée en premier
Un attribut donné doit être appliqué
Dispose doit être appelé
Une option donnée est dangereuse à spécifier
Une API obsolète ne doit pas être utilisée dans le nouveau code

Si l’on se contente de la documentation pour transmettre cela, les utilisateurs risquent de passer à côté.

En intégrant un Analyzer au package NuGet de la bibliothèque, on peut faire apparaître des avertissements directement dans le code de l’utilisateur.

Par exemple, pour une bibliothèque interne Company.Messaging, on peut détecter un mauvais usage comme celui-ci.

var client = new MessageClient();
client.Send(message); // Send appelé avant Configure

L’Analyzer peut alors émettre un avertissement comme celui-ci.

CMP1001 : appelez Configure avant d'appeler MessageClient.Send

Un Code Fix peut en outre proposer une correction candidate ou un exemple de code. Cela améliore l’expérience d’utilisation de la bibliothèque.

Transmettre, directement dans l'éditeur de l'utilisateur, ce qui est écrit dans la documentation

Cette idée constitue l’une des grandes valeurs de Roslyn.

25. Ce qu’il faut prendre en compte pour diffuser un Analyzer via NuGet

Un Analyzer peut être distribué sous forme de package NuGet. Il faut cependant le considérer séparément d’une bibliothèque d’exécution classique, car un Analyzer n’est pas nécessaire à l’exécution de l’application : il est utilisé à la compilation et dans l’IDE.

La conception du package doit donc prendre en compte des points comme ceux-ci.

Faut-il inclure la bibliothèque d'exécution et l'Analyzer dans le même package ?
Faut-il faire de l'Analyzer un package séparé ?
Faut-il émettre des avertissements par défaut ?
Quelle sévérité choisir ?
Faut-il permettre le contrôle via .editorconfig ?
Faut-il éviter de submerger soudainement les utilisateurs existants d'avertissements ?

Pour un usage strictement interne, des règles relativement strictes peuvent être plus facilement acceptées.

Pour une distribution en bibliothèque publique, il faut veiller à ne pas casser soudainement le build des utilisateurs.

Il est souvent plus simple de commencer par Info ou Warning, et de laisser les utilisateurs monter le niveau à Error de leur côté si nécessaire.

26. Roslyn et les fonctionnalités de l’IDE

Dans Visual Studio et les autres environnements de développement .NET, les concepts de Roslyn sont profondément liés aux fonctionnalités de l’IDE.

Par exemple, des fonctionnalités comme celles-ci.

IntelliSense
Go to Definition
Find All References
Rename
Extract Method
Quick Actions
Avertissements de style de code
Détection des using inutilisés

Rien de tout cela n’est réalisable avec une simple recherche de chaîne. Avec Rename, par exemple, il ne faut surtout pas modifier par erreur un autre symbole portant le même nom.

class User
{
    public string Name { get; set; }
}

class Product
{
    public string Name { get; set; }
}

Lorsqu’on veut renommer User.Name, il ne faut pas changer aussi Product.Name. Cela exige de les distinguer non pas seulement par la syntaxe, mais comme des symboles.

Les API de Roslyn constituent une base permettant d’appliquer ce type de fonctionnalité, propre à l’IDE, à ses propres outils.

27. Points d’attention sur la performance

Roslyn est puissant, mais écrire un traitement lourd le ralentira nécessairement. C’est particulièrement vrai pour l’Analyzer et le Source Generator, susceptibles de s’exécuter pendant que le développeur tape ou pendant le build.

Il faut donc être attentif aux points suivants.

Éviter les récupérations inutiles du SemanticModel
Restreindre les candidats via Syntax avant l'analyse sémantique
Éviter les E/S fichier
Ne pas faire d'accès réseau
Éviter la réflexion coûteuse
Respecter les demandes d'annulation
Tenir compte de l'exécution parallèle
Ne pas intégrer d'analyse de la solution entière dans un Analyzer

Dans un Analyzer, il faut restreindre autant que possible les cibles enregistrées dans Initialize.

Un mauvais exemple.

Examiner tous les SyntaxNode, puis trancher en interne avec une multitude d'instructions if

Une meilleure direction.

N'enregistrer que les SyntaxKind nécessaires
Filtrer d'abord légèrement par le nom ou la forme
Ne trancher via le SemanticModel que lorsque c'est nécessaire

Un Analyzer peut rester en permanence actif dans l’environnement de développement de l’utilisateur. La légèreté fait donc partie de la qualité, au même titre que la précision.

28. Concevoir les Diagnostic

Le Diagnostic émis par un Analyzer ne doit pas se contenter d’afficher un avertissement. Lorsqu’un développeur le voit, il doit pouvoir en comprendre les éléments suivants.

Quel est le problème ?
Pourquoi est-ce un problème ?
Où faut-il corriger ?
Comment faut-il corriger ?
Existe-t-il des exceptions ?

Un exemple de mauvais message.

CMP001 : ceci est interdit

Cela ne permet pas de comprendre ce qui ne va pas.

Un exemple allant dans le bon sens.

CMP001 : DateTime.Now dépend de l'heure locale de l'environnement d'exécution. Pour une heure destinée au stockage ou à la comparaison, utilisez DateTimeOffset.UtcNow ou un fournisseur d'heure.

Il est également utile de concevoir les identifiants de Diagnostic à l’avance.

CMP0001-CMP0999: règles communes
CMP1000-CMP1999: règles de la bibliothèque A
CMP2000-CMP2999: règles d'aide à la migration

S’il est possible de préparer une page de documentation, il est aussi utile de définir HelpLinkUri sur le DiagnosticDescriptor.

Un avertissement est une forme de communication avec le développeur. Si le message est bâclé, la règle elle-même finit par ne plus être prise au sérieux.

29. Concevoir les niveaux de sévérité

La sévérité d’un Analyzer doit être choisie avec soin. Voici les principaux niveaux.

Hidden / Silent
Info
Suggestion
Warning
Error

En pratique, il vaut souvent mieux ne pas passer directement à Error. En particulier lorsque le code existant est volumineux, démarrer directement à Error bloque l’adoption.

Concrètement, une adoption progressive comme celle-ci est plus facile à mettre en œuvre.

1. Introduire d'abord la règle en Warning
2. Rendre visible le nombre d'avertissements dans la CI
3. Ne pas ajouter de nouvelles violations
4. Ne passer en Error que les règles importantes
5. Établir un plan pour réduire les violations existantes

L’objectif d’un Analyzer n’est pas de gêner les développeurs, mais d’élever la qualité de la base de code sans forcer.

30. Déboguer un Source Generator

Comme le Source Generator ne s’exécute pas au même endroit qu’une application ordinaire, son débogage présente quelques particularités.

L’investigation s’appuie en général sur les méthodes suivantes.

Examiner la source générée
Émettre des Diagnostic
Écrire des tests
Attacher un débogueur si nécessaire

Dans un projet au format SDK, activer le paramètre qui écrit les fichiers générés sur disque facilite la vérification.

<PropertyGroup>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
  <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

Cela permet de consulter plus facilement les fichiers .g.cs générés.

$(BaseIntermediateOutputPath) désigne généralement un emplacement sous obj/.

Si l’on spécifie un emplacement directement à la racine du projet, comme Generated, alors, comme les projets au format SDK incluent par défaut **/*.cs dans la compilation, les fichiers .g.cs déjà générés seront réintégrés comme sources ordinaires au build suivant, ce qui peut provoquer des erreurs de type ou de membre en double.

S’il faut absolument écrire à la racine du projet, il faut exclure explicitement ces fichiers de la compilation, par exemple avec <Compile Remove="Generated/**/*.cs" />.

Pour tester un Generator, on utilise souvent une comparaison entre le code en entrée et le résultat généré.

Préparer la source en entrée
Exécuter le Generator
Vérifier la source générée
Vérifier les Diagnostic attendus

Un Source Generator vérifié uniquement à la main se casse vite. Plus la logique de génération se complexifie, plus les tests deviennent importants.

31. Tester avec Roslyn

Les Analyzer et les Source Generator doivent être développés en écrivant des tests. Pour l’Analyzer en particulier, les faux positifs comme les détections manquées posent problème.

Les tests doivent couvrir des cas comme ceux-ci.

Du code qui doit être détecté
Du code qui ne doit pas être détecté
Du code utilisant un using alias
Du code utilisant un nom pleinement qualifié
Du code utilisant un type différent portant un nom similaire
Du code traité comme du code généré
Du code avec nullable activé

Par exemple, pour un Analyzer qui interdit System.DateTime.Now, on vérifiera des cas comme ceux-ci.

// Doit être détecté
var x = System.DateTime.Now;
// Doit aussi être détecté lorsqu'un using est présent
using System;
var x = DateTime.Now;
// Ne doit pas être détecté s'il s'agit d'un autre type
namespace MyCompany;

public static class DateTime
{
    public static string Now => "now";
}

var x = DateTime.Now;

Ce dernier cas est un exemple typique où une recherche de chaîne se trompe facilement. Un Analyzer Roslyn l’évite en vérifiant le symbole cible via le SemanticModel.

32. Peut-on l’utiliser avec des projets .NET Framework ?

Roslyn n’est pas réservé au .NET actuel. Cependant, les points de vigilance varient selon la manière dont on l’utilise.

En tant qu’outil d’investigation

Construire un outil Roslyn sous forme d’application console .NET 8 ou .NET 10, puis charger et analyser une solution .NET Framework, est une option réaliste.

Dans ce cas, l’outil lui-même tourne sur le .NET actuel, tandis que la cible d’analyse peut être du code .NET Framework.

Cependant, pour charger une solution via MSBuildWorkspace, il faut disposer d’un environnement capable de builder les projets ciblés : MSBuild, le SDK, les assemblies de référence, et un environnement de restauration NuGet.

Autrement dit, Roslyn seul ne permet pas de tout lire : résoudre la configuration réelle d’un projet exige un environnement de build.

En tant qu’Analyzer

L’Analyzer fonctionne en étant chargé par le compilateur ou l’IDE.

Même si le projet ciblé est en .NET Framework, il reste utilisable dès lors que l’environnement permet au compilateur de charger l’Analyzer.

Toutefois, avec un ancien format de csproj, une ancienne version de Visual Studio ou de MSBuild, ou une configuration basée sur packages.config, l’introduction et l’exploitation peuvent être moins simples qu’avec un projet au format SDK actuel.

Pour l’introduire dans un projet .NET Framework existant, il est utile de vérifier d’abord les points suivants.

La version de Visual Studio / MSBuild
La possibilité d'utiliser PackageReference
Le fonctionnement du même Analyzer sur la CI
L'apparition des avertissements dans le journal de build
La prise en compte de .editorconfig

En tant que Source Generator

Le Source Generator est un mécanisme chargé par le compilateur à la compilation.

Ce qui compte donc, plus que le framework d’exécution du projet ciblé, c’est le niveau de prise en charge du compilateur et du SDK utilisés pour le build.

Si les projets au format SDK du .NET actuel restent faciles à manier, les anciens projets .NET Framework demandent de la prudence, selon le format du projet et l’environnement de build.

Pour un existant en .NET Framework, il est souvent plus sûr de commencer par des outils d’investigation ou des Analyzer basés sur Roslyn, plutôt que d’intégrer un Source Generator dès le départ.

33. Bien choisir les versions

Les packages NuGet liés à Roslyn appartiennent à la famille Microsoft.CodeAnalysis.*.

Voici les principaux.

Microsoft.CodeAnalysis.CSharp
Microsoft.CodeAnalysis.CSharp.Workspaces
Microsoft.CodeAnalysis.Workspaces.MSBuild
Microsoft.CodeAnalysis.Analyzers
Microsoft.CodeAnalysis.CSharp.CodeFix.Testing
Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing

Le point à retenir ici est que l’Analyzer et le Source Generator sont chargés par le compilateur du côté de l’utilisateur.

Autrement dit, si le SDK / Visual Studio du poste du développeur ou de la CI est ancien, un Analyzer / Generator utilisant des API Roslyn trop récentes risque de ne pas fonctionner.

Pour un usage strictement interne où l’on peut uniformiser l’environnement de build, il est plus facile d’utiliser des API relativement récentes.

En revanche, pour une bibliothèque distribuée à l’extérieur, il faut choisir avec prudence la version de Microsoft.CodeAnalysis dont on dépend, en tenant compte de la diversité des environnements des utilisateurs.

En guise de politique, on peut retenir ceci.

Usage interne         : aligner la CI et l'environnement de développement, puis utiliser des API plus récentes
Distribution externe  : choisir de façon conservatrice en tenant compte de la plage de SDK/VS des utilisateurs
Generator             : le concevoir comme un Incremental Generator si possible
Analyzer               : privilégier une légèreté qui ne dégrade pas l'expérience de l'IDE

Roslyn étant proche du compilateur, il est particulièrement sensible aux écarts de version.

34. Ne pas chercher à tout faire avec Roslyn

Roslyn est puissant, mais ce n’est pas un outil qui résout tous les problèmes. Par exemple, les problèmes suivants ne peuvent pas être résolus par Roslyn seul.

Quelle branche est empruntée à l'exécution
Quelles valeurs arrivent avec les données de production
Les méthodes appelées dynamiquement par réflexion
Le résultat de l'enregistrement à l'exécution d'un conteneur d'injection de dépendances
Un traitement qui varie selon un fichier de configuration
Les valeurs renvoyées par un service externe

Roslyn est avant tout un outil qui traite le code source et les informations de compilation. Pour connaître le comportement à l’exécution, il faut recourir à d’autres moyens : tests, journaux, traces, profilage, analyse de dump, etc.

Le rôle de Roslyn peut donc se résumer ainsi.

Traiter avec une grande précision ce qui peut être connu statiquement

Vouloir forcer Roslyn à résoudre ce qui ne peut être connu que dynamiquement aboutit à un mécanisme complexe et imprécis.

35. L’ordre d’adoption

Pour commencer à utiliser Roslyn en pratique, cet ordre est recommandé.

1. Mettre en ordre les Analyzer .NET existants et .editorconfig
2. Écrire un petit outil d'investigation utilisant le Syntax Tree
3. Essayer la résolution de types avec le SemanticModel
4. Lire une solution avec MSBuildWorkspace
5. Créer un petit Analyzer spécifique à l'équipe
6. Ajouter un Code Fix si nécessaire
7. Envisager un Source Generator là où le code répétitif abonde

Il n’est pas nécessaire de se lancer directement dans le Source Generator. Sur la plupart des terrains, l’Analyzer et les outils d’investigation produisent des résultats plus rapidement.

En particulier lorsque l’existant est volumineux, un déroulement comme celui-ci est réaliste.

Comprendre l'état actuel avec un outil d'investigation
Transformer les problèmes fréquents en Analyzer
Ne transformer en Code Fix que ce qui peut être corrigé en toute sécurité
Transformer en Generator le code répétitif qu'on écrit sans cesse

Roslyn est un outil qu’on peut adopter progressivement.

36. Petit exemple : lister les appels de méthode

Pour terminer, voyons un usage de Roslyn un peu plus proche de la pratique. Voici une esquisse qui liste les appels de méthode dans une solution.

using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.MSBuild;

MSBuildLocator.RegisterDefaults();

using var workspace = MSBuildWorkspace.Create();
var solution = await workspace.OpenSolutionAsync(args[0]);

foreach (var project in solution.Projects)
{
    var compilation = await project.GetCompilationAsync();
    if (compilation is null)
    {
        continue;
    }

    foreach (var document in project.Documents)
    {
        var tree = await document.GetSyntaxTreeAsync();
        if (tree is null)
        {
            continue;
        }

        var root = await tree.GetRootAsync();
        var semanticModel = compilation.GetSemanticModel(tree);

        var invocations = root
            .DescendantNodes()
            .OfType<InvocationExpressionSyntax>();

        foreach (var invocation in invocations)
        {
            var symbol = semanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol;
            if (symbol is null)
            {
                continue;
            }

            var lineSpan = invocation.GetLocation().GetLineSpan();
            var line = lineSpan.StartLinePosition.Line + 1;

            Console.WriteLine(string.Join(",", new[]
            {
                project.Name,
                document.FilePath ?? document.Name,
                line.ToString(),
                symbol.ContainingType.ToDisplayString(),
                symbol.Name
            }));
        }
    }
}

En étendant un peu un tel outil, on peut mener des investigations comme celles-ci.

Extraire uniquement les appels à une méthode donnée
Sortir les emplacements utilisant une API obsolète
Sortir la fréquence d'utilisation par projet
Construire la liste des API concernées par la migration

Pouvoir lire le code source du point de vue du compilateur rend l’étude du code existant nettement plus simple.

37. Précautions lors de la réécriture de code avec Roslyn

Roslyn permet également de réécrire du code en s’appuyant sur l’arbre syntaxique.

On peut par exemple renommer une méthode, ajouter un attribut, ou ajouter un using.

La réécriture de code doit toutefois être menée avec prudence. Voici les points de vigilance.

Vérifier que le sens ne change pas
Ne pas casser les commentaires ni les espaces
Éviter que le diff ne devienne trop volumineux
Uniformiser le formatage
Ne pas effectuer trop de transformations à la fois
Faciliter la relecture du diff Git

Comme le Syntax Tree de Roslyn conserve le Trivia, il est possible d’effectuer des transformations qui préservent les commentaires et les espaces. Mais si l’on construit les nœuds sans soin, la mise en forme du code généré peut se dégrader.

Lors de la création d’un outil de réécriture, cette approche est plus sûre.

Commencer par ne faire que de la détection
Vérifier le diff avant/après transformation
Commencer par de petites transformations
Écrire des tests pour l'outil de transformation lui-même
Démarrer en mode détection sur la CI

Pour les transformations mécaniques à grande échelle, Roslyn est puissant, mais une revue humaine reste nécessaire au final.

38. Roslyn et l’assistance au codage par IA

Ces dernières années, la génération de code et l’assistance à la revue par IA se sont généralisées, mais la valeur de Roslyn n’en diminue pas pour autant. L’IA excelle dans le traitement du langage naturel et du contexte environnant, tandis que Roslyn, en tant que compilateur, excelle dans le traitement précis des informations syntaxiques et sémantiques. Les deux sont davantage complémentaires que concurrents.

On peut par exemple envisager une répartition comme celle-ci.

Extraire précisément les emplacements ciblés avec Roslyn
Générer la stratégie de correction et les explications avec l'IA
Vérifier avec Roslyn que la proposition de correction compile
Prévenir la récurrence avec un Analyzer

Il peut être plus sûr d’extraire précisément les emplacements ciblés avec Roslyn plutôt que de demander à l’IA de « corriger toutes les API obsolètes de cette base de code ».

Utiliser ensuite l’IA pour examiner la stratégie de correction et assister la revue est plus pragmatique.

Laisser au compilateur ce que le compilateur sait faire. Laisser les humains et l’IA se concentrer sur les décisions de niveau supérieur.

Cette répartition des rôles est essentielle.

39. Check-list pratique

Avant d’utiliser Roslyn, il est utile de vérifier les points suivants.

L'objectif est-il l'investigation, l'avertissement, la correction ou la génération ?
La syntaxe seule suffit-elle, ou l'analyse sémantique est-elle nécessaire ?
Un seul fichier suffit-il, ou faut-il le projet entier ?
Faut-il l'exécuter dans l'IDE, ou un outil ponctuel suffit-il ?
Un impact sur le temps de build est-il acceptable ?
Sera-t-il exécuté sur la CI ?
Le code existant va-t-il générer une avalanche d'avertissements ?
Quelle sévérité donner à l'Analyzer ?
Le Code Fix peut-il être appliqué en toute sécurité ?
Le code généré par le Source Generator est-il consultable ?
Les versions de SDK / Visual Studio des utilisateurs sont-elles alignées ?

En cas d’hésitation, cette répartition est utile.

Vouloir investiguer                 -> un outil console basé sur Roslyn
Vouloir toujours faire respecter    -> un Analyzer
La façon de corriger est établie    -> un Code Fix
Vouloir produire du code répétitif  -> un Source Generator

Cette répartition réduit le risque de se tromper sur le bon usage de Roslyn.

40. Résumé

Roslyn ouvre le compilateur C# et Visual Basic sous forme d’API que les développeurs peuvent utiliser.

Avec Roslyn, on peut traiter le code source non pas comme une simple chaîne de caractères, mais sous ces formes.

Lire la syntaxe comme un Syntax Tree
Lire le sens via le SemanticModel
Traiter l'ensemble de la compilation comme une Compilation
Traiter la solution ou le projet comme un Workspace
Émettre des avertissements en tant qu'Analyzer
Proposer des corrections en tant que Code Fix
Générer du code en tant que Source Generator

En pratique, Roslyn est particulièrement utile dans les situations suivantes.

Étude d'une base de code existante
Aide à la migration de .NET Framework vers .NET
Vérification automatique des conventions d'équipe
Guidage des utilisateurs d'une bibliothèque
Génération de code répétitif
Assurance qualité dans l'IDE et sur la CI

L’essentiel est de ne pas trop se braquer face à Roslyn en le considérant comme une « technologie de compilateur difficile ».

Au départ, il suffit de lire un seul fichier avec CSharpSyntaxTree.ParseText et d’énumérer les noms de méthode. On peut ensuite étendre progressivement vers le SemanticModel, le Workspace, l’Analyzer et le Source Generator.

En une phrase, on pourrait résumer Roslyn ainsi.

Il permet de traiter le code C# non pas comme une chaîne de caractères, mais comme la structure comprise par le compilateur.

Adopter ce point de vue facilite d’un cran l’automatisation de la revue de code, de la migration, de l’investigation et de la génération.

Références

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

Qu'est-ce que Roslyn et que peut-on en faire ?
Roslyn, officiellement le .NET Compiler Platform, est l'implémentation du compilateur C# et Visual Basic exposée sous forme d'API que les outils et applications peuvent utiliser. Il permet d'analyser le code en Syntax Tree, de résoudre le sens via le SemanticModel, d'analyser des solutions entières via des Workspace, de créer des Analyzer qui émettent des avertissements à la compilation, des Code Fix qui proposent des corrections, et des Source Generator qui ajoutent du code à la compilation. Il permet de traiter le code C# comme la structure comprise par le compilateur, et non comme une simple chaîne de caractères.
Pourquoi utiliser Roslyn plutôt que des expressions régulières pour rechercher dans du code C# ?
Parce que le C# n'est pas une simple chaîne de caractères. Une recherche par expression régulière sur Console.WriteLine capture aussi le texte des commentaires, les littéraux de chaîne, les appels répartis sur plusieurs lignes, ou des types différents introduits via un alias using. Roslyn distingue la syntaxe des symboles résolus, ce qui permet de confirmer qu'un appel désigne réellement System.Console.WriteLine et non un type sans rapport portant le même nom. Pour une recherche approximative, grep suffit souvent ; mais si les résultats servent à des décisions de conception ou à des corrections automatisées, Roslyn est le choix le plus sûr.
Quelle est la différence entre un Analyzer et un Source Generator ?
Un Analyzer examine le code et signale des Diagnostic — avertissements ou erreurs — dans l'IDE et à la compilation ; il agit comme une revue de code automatisée pour des règles telles qu'interdire DateTime.Now ou imposer des conventions de nommage. Un Source Generator s'exécute à la compilation et ajoute du code C# généré à la même compilation ; il ne réécrit pas le code existant. Utilisez l'Analyzer pour faire respecter une règle, le Code Fix quand la correction est mécanique et sûre, et le Source Generator pour remplacer du code répétitif écrit à la main, comme le mapping de DTO ou les métadonnées de sérialisation.
Peut-on utiliser Roslyn avec des projets .NET Framework ?
Oui, avec des réserves qui dépendent de l'usage. Un outil d'investigation construit comme une application console .NET moderne peut charger et analyser une solution .NET Framework, bien que MSBuildWorkspace ait besoin d'un environnement de build fonctionnel pour les projets ciblés. Les Analyzer fonctionnent tant que le compilateur de l'environnement peut les charger, mais les anciens formats de csproj, les anciennes versions de Visual Studio et les configurations basées sur packages.config peuvent être moins fluides que les projets au format SDK. Pour un existant .NET Framework ancien, il est plus sûr de commencer par des outils d'investigation et des Analyzer plutôt que d'intégrer un Source Generator dès le départ.

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.

Retour au blog