Génération de code avec Roslyn

De temps en temps, quand je lisais sur Roslyn et ses analyseurs, je pensais constamment: "Mais vous pouvez faire des pépites avec cette chose, qui fera le tour du code et fera la génération de code." Une recherche rapide n'a rien montré d'intéressant, il a donc été décidé de creuser. Comme j'ai été agréablement surpris quand j'ai découvert que mon idée était non seulement réalisable, mais tout cela fonctionnerait presque sans béquilles.


Et alors, qui est intéressé à voir comment vous pouvez faire une "petite réflexion" et l'emballer dans une pépite, s'il vous plaßt, sous cat.


Présentation


Je pense que la premiÚre chose à clarifier est ce que l'on entend par "peu de réflexion". Je suggÚre d'implémenter pour tous les types la méthode Dictionary<string, Type> GetBakedType() , qui renverra les noms des propriétés et leurs types. Comme cela devrait fonctionner avec tous les types, l'option la plus simple serait de générer une méthode d'extension pour chaque type. Son implémentation manuelle aura la forme suivante:


 using System; using System.Collections.Generic; public static class testSimpleReflectionUserExtentions { private static Dictionary<string, Type> properties = new Dictionary<string, Type> { { "Id", typeof(System.Guid)}, { "FirstName", typeof(string)}, { "LastName", typeof(string)}, }; public static Dictionary<string, Type> GetBakedType(this global::testSimpleReflection.User value) { return properties; } } 

Il n'y a rien de surnaturel ici, mais le rĂ©aliser pour tous les types est une affaire morne et pas intĂ©ressante qui, de plus, menace les fautes de frappe. Pourquoi ne demandons-nous pas l'aide du compilateur? C'est lĂ  que Roslyn et ses analyseurs entrent dans l'arĂšne. Ils permettent d'analyser le code et de le modifier. Apprenons donc au compilateur une nouvelle astuce. Laissez-le parcourir le code et voir oĂč nous l'utilisons, mais nous n'avons pas encore implĂ©mentĂ© notre GetBakedType et l'implĂ©mente.


Pour "activer" cette fonctionnalitĂ©, nous n'avons besoin d'installer qu'un seul paquet de pĂ©pites et tout fonctionnera immĂ©diatement. Ensuite, appelez simplement GetBakedType si nĂ©cessaire, nous obtenons une erreur de compilation qui dit que la rĂ©flexion pour ce type n'est pas encore prĂȘte, appelez codefix et vous avez terminĂ©. Nous avons une mĂ©thode d'extension qui nous renverra toutes les propriĂ©tĂ©s publiques.


Je pense que dans le mouvement, il sera plus facile de comprendre comment cela fonctionne généralement, voici une courte visualisation:



Toute personne intéressée à essayer ceci localement peut installer le paquet nuget sous le nom SimpleReflection:


 Install-Package SimpleReflection 

Peu importe la source, ils sont lĂ  .


Je veux avertir que cette implémentation n'est pas conçue pour une utilisation réelle. Je veux juste montrer un moyen d'organiser la génération de code avec Roslyn.


Préparation préliminaire


Avant de commencer à créer vos analyseurs, vous devez installer le composant «Développement d'extension Visual Studio» dans le programme d'installation du studio. Pour VS 2019, vous devez vous souvenir de sélectionner le ".NET Compiler Platform SDK" comme composant facultatif.


Implémentation de l'analyseur


Je ne décrirai pas par étapes comment implémenter l'analyseur, car il est trÚs simple, mais il suffit de passer par les points clés.


Et le premier point clé sera que si nous avons une vraie erreur de compilation, alors les analyseurs ne démarrent pas du tout. Par conséquent, si nous essayons d'appeler notre GetBakedType() dans le contexte du type pour lequel il n'est pas implémenté, nous obtenons une erreur de compilation et tous nos efforts n'auront aucun sens. Mais ici, nous serons aidés par la connaissance de la priorité avec laquelle le compilateur appelle les méthodes d'extension. Le fait est que les implémentations spécifiques ont priorité sur les méthodes génériques. Autrement dit, dans l'exemple suivant, la deuxiÚme méthode sera appelée, pas la premiÚre:


 public static class SomeExtentions { public static void Save<T>(this T value) { ... } public static void Save(this User user) { ... } } public class Program { public static void Main(string[] args) { var user = new User(); user.Save(); } } 

Cette fonctionnalité est trÚs pratique. Nous définissons simplement le GetBakedType générique comme suit:


 using System; using System.Collections.Generic; public static class StubExtention { public static Dictionary<string, Type> GetBakedType<TValue>(this TValue value) { return new Dictionary<string, Type>(); } } 

Cela nous permettra d'éviter une erreur de compilation au tout début et de générer notre propre "erreur" de compilation.


ConsidĂ©rez l'analyseur lui-mĂȘme. Il offrira deux diagnostics. Le premier est responsable du cas oĂč la gĂ©nĂ©ration de code n'a pas commencĂ© du tout, et le second lorsque nous devons mettre Ă  jour un code existant. Ils auront respectivement les noms SimpleReflectionIsNotReady et SimpleReflectionUpdate . Le premier diagnostic gĂ©nĂšre une "erreur de compilation" et le second signale uniquement qu'ici, vous pouvez exĂ©cuter Ă  nouveau la gĂ©nĂ©ration de code.


La description des diagnostics est la suivante:


  public const string SimpleReflectionIsNotReady = "SimpleReflectionIsNotReady"; public const string SimpleReflectionUpdate = "SimpleReflectionUpdate"; public static DiagnosticDescriptor SimpleReflectionIsNotReadyDescriptor = new DiagnosticDescriptor( SimpleReflectionIsNotReady, "Simple reflection is not ready.", "Simple reflection is not ready.", "Codegen", DiagnosticSeverity.Error, isEnabledByDefault: true, "Simple reflection is not ready."); public static DiagnosticDescriptor SimpleReflectionUpdateDescriptor = new DiagnosticDescriptor( SimpleReflectionUpdate, "Simple reflection update.", "Simple reflection update.", "Codegen", DiagnosticSeverity.Info, isEnabledByDefault: true, "Simple reflection update."); 

Ensuite, il est nécessaire de déterminer ce que nous chercherons, dans ce cas ce sera un appel de méthode:


 public override void Initialize(AnalysisContext context) { context.RegisterOperationAction(this.HandelBuilder, OperationKind.Invocation); } 

Ensuite, dĂ©jĂ  dans HandelBuilder y a une analyse de l'arbre de syntaxe. À l'entrĂ©e, nous recevrons tous les appels trouvĂ©s, vous devez donc tout Ă©liminer sauf notre GetBakedType . Cela peut ĂȘtre fait avec l'habituel if nous vĂ©rifions le nom de la mĂ©thode. Ensuite, nous obtenons le type de variable sur laquelle notre mĂ©thode est appelĂ©e et informons le compilateur des rĂ©sultats de notre analyse. Cela peut ĂȘtre une erreur de compilation si la gĂ©nĂ©ration de code n'a pas encore commencĂ© ou la possibilitĂ© de la redĂ©marrer.


Tout ressemble Ă  ceci:


 private void HandelBuilder(OperationAnalysisContext context) { if (context.Operation.Syntax is InvocationExpressionSyntax invocation && invocation.Expression is MemberAccessExpressionSyntax memberAccess && memberAccess.Name is IdentifierNameSyntax methodName && methodName.Identifier.ValueText == "GetBakedType") { var semanticModel = context.Compilation .GetSemanticModel(invocation.SyntaxTree); var typeInfo = semanticModel .GetSpeculativeTypeInfo(memberAccess.Expression.SpanStart, memberAccess.Expression, SpeculativeBindingOption.BindAsExpression); var diagnosticProperties = ImmutableDictionary<string, string>.Empty.Add("type", typeInfo.Type.ToDisplayString()); if (context.Compilation.GetTypeByMetadataName(typeInfo.Type.GetSimpleReflectionExtentionTypeName()) is INamedTypeSymbol extention) { var updateDiagnostic = Diagnostic.Create(SimpleReflectionUpdateDescriptor, methodName.GetLocation(), diagnosticProperties); context.ReportDiagnostic(updateDiagnostic); return; } var diagnostic = Diagnostic.Create(SimpleReflectionIsNotReadyDescriptor, methodName.GetLocation(), diagnosticProperties); context.ReportDiagnostic(diagnostic); } } 

Code analyseur complet
  [DiagnosticAnalyzer(LanguageNames.CSharp)] public class SimpleReflectionAnalyzer : DiagnosticAnalyzer { public const string SimpleReflectionIsNotReady = "SimpleReflectionIsNotReady"; public const string SimpleReflectionUpdate = "SimpleReflectionUpdate"; public static DiagnosticDescriptor SimpleReflectionIsNotReadyDescriptor = new DiagnosticDescriptor( SimpleReflectionIsNotReady, "Simple reflection is not ready.", "Simple reflection is not ready.", "Codegen", DiagnosticSeverity.Error, isEnabledByDefault: true, "Simple reflection is not ready."); public static DiagnosticDescriptor SimpleReflectionUpdateDescriptor = new DiagnosticDescriptor( SimpleReflectionUpdate, "Simple reflection update.", "Simple reflection update.", "Codegen", DiagnosticSeverity.Info, isEnabledByDefault: true, "Simple reflection update."); public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(SimpleReflectionIsNotReadyDescriptor, SimpleReflectionUpdateDescriptor); public override void Initialize(AnalysisContext context) { context.RegisterOperationAction(this.HandelBuilder, OperationKind.Invocation); } private void HandelBuilder(OperationAnalysisContext context) { if (context.Operation.Syntax is InvocationExpressionSyntax invocation && invocation.Expression is MemberAccessExpressionSyntax memberAccess && memberAccess.Name is IdentifierNameSyntax methodName && methodName.Identifier.ValueText == "GetBakedType" ) { var semanticModel = context.Compilation .GetSemanticModel(invocation.SyntaxTree); var typeInfo = semanticModel .GetSpeculativeTypeInfo(memberAccess.Expression.SpanStart, memberAccess.Expression, SpeculativeBindingOption.BindAsExpression); var diagnosticProperties = ImmutableDictionary<string, string>.Empty.Add("type", typeInfo.Type.ToDisplayString()); if (context.Compilation.GetTypeByMetadataName(typeInfo.Type.GetSimpleReflectionExtentionTypeName()) is INamedTypeSymbol extention) { var updateDiagnostic = Diagnostic.Create(SimpleReflectionUpdateDescriptor, methodName.GetLocation(), diagnosticProperties); context.ReportDiagnostic(updateDiagnostic); return; } var diagnostic = Diagnostic.Create(SimpleReflectionIsNotReadyDescriptor, methodName.GetLocation(), diagnosticProperties); context.ReportDiagnostic(diagnostic); } } } 

Implémentation du générateur de code


Nous effectuerons CodeFixProvider génération de CodeFixProvider via CodeFixProvider , qui est abonné à notre analyseur. Tout d'abord, nous devons vérifier ce qui s'est passé pour trouver notre analyseur.


Cela ressemble Ă  ceci:


 public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) { var diagnostic = context.Diagnostics.First(); var title = diagnostic.Severity == DiagnosticSeverity.Error ? "Generate simple reflection" : "Recreate simple reflection"; context.RegisterCodeFix( CodeAction.Create( title, createChangedDocument: token => this.CreateFormatterAsync(context, diagnostic, token), equivalenceKey: title), diagnostic); } 

Toute magie se produit à l'intérieur de CreateFormatterAsync . Nous y trouvons une description complÚte du type. Ensuite, nous commençons la génération de code et ajoutons un nouveau fichier au projet.


Obtenir des informations et ajouter un fichier:


  private async Task<Document> CreateFormatterAsync(CodeFixContext context, Diagnostic diagnostic, CancellationToken token) { var typeName = diagnostic.Properties["type"]; var currentDocument = context.Document; var model = await context.Document.GetSemanticModelAsync(token); var symbol = model.Compilation.GetTypeByMetadataName(typeName); var rawSource = this.BuildSimpleReflection(symbol); var source = Formatter.Format(SyntaxFactory.ParseSyntaxTree(rawSource).GetRoot(), new AdhocWorkspace()).ToFullString(); var fileName = $"{symbol.GetSimpleReflectionExtentionTypeName()}.cs"; if (context.Document.Project.Documents.FirstOrDefault(o => o.Name == fileName) is Document document) { return document.WithText(SourceText.From(source)); } var folders = new[] { "SimpeReflection" }; return currentDocument.Project .AddDocument(fileName, source) .WithFolders(folders); } 

Génération de code propre (je soupçonne que le concentrateur cassera tout le sous-réseau):


 private string BuildSimpleReflection(INamedTypeSymbol symbol) => $@" using System; using System.Collections.Generic; // Simple reflection for {symbol.ToDisplayString()} public static class {symbol.GetSimpleReflectionExtentionTypeName()} {{ private static Dictionary<string, Type> properties = new Dictionary<string, Type> {{ { symbol .GetAllMembers() .OfType<IPropertySymbol>() .Where(o => (o.DeclaredAccessibility & Accessibility.Public) > 0) .Select(o => $@" {{ ""{o.Name}"", typeof({o.Type.ToDisplayString()})}},") .JoinWithNewLine() } }}; public static Dictionary<string, Type> GetBakedType(this global::{symbol.ToDisplayString()} value) {{ return properties; }} }} "; } 

Générateur de code complet
 using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Text; using SimpleReflection.Utils; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace SimpleReflection { [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SimpleReflectionCodeFixProvider)), Shared] public class SimpleReflectionCodeFixProvider : CodeFixProvider { public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(SimpleReflectionAnalyzer.SimpleReflectionIsNotReady, SimpleReflectionAnalyzer.SimpleReflectionUpdate); public sealed override FixAllProvider GetFixAllProvider() { return WellKnownFixAllProviders.BatchFixer; } public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) { var diagnostic = context.Diagnostics.First(); var title = diagnostic.Severity == DiagnosticSeverity.Error ? "Generate simple reflection" : "Recreate simple reflection"; context.RegisterCodeFix( CodeAction.Create( title, createChangedDocument: token => this.CreateFormatterAsync(context, diagnostic, token), equivalenceKey: title), diagnostic); } private async Task<Document> CreateFormatterAsync(CodeFixContext context, Diagnostic diagnostic, CancellationToken token) { var typeName = diagnostic.Properties["type"]; var currentDocument = context.Document; var model = await context.Document.GetSemanticModelAsync(token); var symbol = model.Compilation.GetTypeByMetadataName(typeName); var symbolName = symbol.ToDisplayString(); var rawSource = this.BuildSimpleReflection(symbol); var source = Formatter.Format(SyntaxFactory.ParseSyntaxTree(rawSource).GetRoot(), new AdhocWorkspace()).ToFullString(); var fileName = $"{symbol.GetSimpleReflectionExtentionTypeName()}.cs"; if (context.Document.Project.Documents.FirstOrDefault(o => o.Name == fileName) is Document document) { return document.WithText(SourceText.From(source)); } var folders = new[] { "SimpeReflection" }; return currentDocument.Project .AddDocument(fileName, source) .WithFolders(folders); } private string BuildSimpleReflection(INamedTypeSymbol symbol) => $@" using System; using System.Collections.Generic; // Simple reflection for {symbol.ToDisplayString()} public static class {symbol.GetSimpleReflectionExtentionTypeName()} {{ private static Dictionary<string, Type> properties = new Dictionary<string, Type> {{ { symbol .GetAllMembers() .OfType<IPropertySymbol>() .Where(o => (o.DeclaredAccessibility & Accessibility.Public) > 0) .Select(o => $@" {{ ""{o.Name}"", typeof({o.Type.ToDisplayString()})}},") .JoinWithNewLine() } }}; public static Dictionary<string, Type> GetBakedType(this global::{symbol.ToDisplayString()} value) {{ return properties; }} }} "; } } 

Résumé


En consĂ©quence, nous avons un gĂ©nĂ©rateur de code analyseur Roslyn Ă  l'aide duquel une "petite" rĂ©flexion utilisant la gĂ©nĂ©ration de code est implĂ©mentĂ©e. Il sera difficile de trouver une vĂ©ritable application pour la bibliothĂšque actuelle, mais ce sera un excellent exemple pour implĂ©menter des gĂ©nĂ©rateurs de code facilement accessibles. Cette approche peut ĂȘtre, comme toute gĂ©nĂ©ration de code, utile pour Ă©crire des sĂ©rialiseurs. Mon implĂ©mentation de test de MessagePack a fonctionnĂ© ~ 20% plus rapidement que neuecc / MessagePack-CSharp , et je n'ai pas encore vu de sĂ©rialiseur plus rapide. De plus, cette approche ne nĂ©cessite pas Roslyn.Emit , ce qui est parfait pour les scĂ©narios Unity et AOT.

Source: https://habr.com/ru/post/fr455952/


All Articles