Le 19 octobre, Microsoft a publié la première CTP de Roslyn.
Difficile d’aller plus loin dans la compréhension de Roslyn sans préalablement définir ce qu’est Roslyn. Roslyn est le nom de code d’un projet qui permet d'exposer les API des compilateurs C# et VB.NET.
Précédemment ce projet s’appelait Compiler as Service. Celui-ci a été présenté par Anders Hejlsberg dès la PDC 2008. A l’époque, il permettait de compiler à la volée des instructions et de les exécuter dans la foulée.
Le nom du projet a changé afin de marquer une vraie rupture avec ces démos car Roslyn ne se limite désormais plus à ça et permet bien d’autres scenarii comme nous allons le voir. De plus, le terme de Service est un peu trop utilisé, notamment avec le Cloud ce qui portait à confusion.
Aujourd’hui Roslyn couvre essentiellement 5 grands domaines :
Vous pouvez télécharger Roslyn ici. L’installation peut se faire avec Visual Studio 2010 à condition que le SP1 soit installé.
A la manière de ce qui existait déjà en F#, l’installation de Roslyn rajoute la C# interactive Window dans laquelle vous allez pouvoir évaluer des expressions (par exemple « 1 + 1 ») ou définir des instructions (par exemple définir une variable qui sera ensuite utilisable dans le reste de la fenêtre). A noter que cette fenêtre possède l’intellisense.
Un certain nombre de commandes sont disponibles avec le caractère #. Par exemple, #r vous permettra de rajouter une référence dans l’environnement de la fenêtre.
Vous pouvez retrouver ces commandes grâce à la commande « #help ».
Pour plus d’information la compilation à la volée du code, je vous invite à consulter les deux liens suivants :
Toujours dans le même esprit, il est possible, grâce à Roslyn d’utiliser C# ou VB.NET comme langage de scripting.
Pour hoster dynamiquement un script C#, il vous suffit d’instancier une variable de type Roslyn.Scripting.CSharp.ScriptEngine en lui précisant les références.
ScriptEngine csharpEngine = new ScriptEngine(new[]
{ "System", "System.Xaml", "PresentationFramework", "PresentationCore", "WindowsBase", this.GetType().Assembly.Location
});
Ensuite, il faut créer une instance de Roslyn.Scripting.Session.
Pour cela, il faut utiliser la méthode statique Create sur la classe Session. Celle-ci a deux définitions : une avec paramètre, une sans.
Dans le cas où le script devrait manipuler des objets du host, il faudra utiliser l’instance avec paramètre.
Session session = Session.Create(new HostObjectModel(this));
Suite à cela, il est possible d’exécuter notre script avec la méthode Execute sur la classe ScriptEngine.
var foo = csharpEngine.Execute(myScriptText, session);
La méthode Execute existe aussi en version générique.
Le paramètre option est facultatif. Il n’est utile que si l’on souhaite réutiliser dans un autre script les éléments de codes déclarés dans le script (variables, méthodes, classes, etc.).
Avec Roslyn, il est possible de récupérer un arbre syntaxique à partir d’un code source. Celui-ci est, dans la philosophie, très proche des arbres d’expressions introduits avec LINQ.
Le fait d’avoir accès à l’arbre syntaxique d’un code va nous permettre de l’analyser mais aussi de le modifier.
Il est également possible d’accéder aux symboles utilisés pour la compilation. Combinés avec l’arbre syntaxique, il est possible de récupérer des informations sur les nœuds de l’arbre tel que le namespace ou encore l’assembly d’un type par exemple.
Lorsque l’on installe Roslyn, de nouveaux templates de projets sont ajoutés à Visual Studio.
.png)
Dans le cadre de cet article, nous allons nous concentrer sur les templates « Code Issue » et « Code Refactoring ».
Ces templates permettent de développer des extensions à Visual Studio.
Dans le cadre de cet article, nous allons analyser des exemples fournis avec la CTP.
Commençons par le projet SpecifyAccessibilityCS.
En l’exécutant, il ouvre un nouveau Visual Studio nous permettant de tester notre extension.
Si vous créez un nouveau projet de type Console, il va, de façon tout à fait classique, vous générer un fichier program.cs avec le code suivant :
.png)
Remarquez le petit trait bleu qui nous indique qu’une action est possible.
Si l’on regarde les erreurs, on s’aperçoit qu’il y a deux warnings :
.png)
Cet exemple de code, vous l’aurez compris, crée un warning dès lors qu’un champ / méthode / propriété / classe / interface n’a pas défini de visibilité.
Si l’on positionne la souris sur le trait bleu, Visual Studio nous propose de rajouter la visibilité.
.png)
Comment ça marche ?
Dès lors qu’on crée un projet de type « Code Issue », on a deux fichiers : un fichier .vsixmanifest et un fichier CodeIssueProvider.cs.
Le premier est utilisé pour l’extensibilité de Visual Studio, le second contient le code qui va nous intéresser.
Cette classe est exportée au sens MEF du terme et implémente l’interface Roslyn.Services.Editor.ICodeIssueProvider
[ExportSyntaxNodeCodeIssueProvider("SpecifyAccessibilityCS", LanguageNames.CSharp, typeof(MemberDeclarationSyntax))]internal class CodeIssueProvider : ICodeIssueProvider
{La classe ExportSyntaxNodeCodeIssueProviderAttribute hérite de la classe System.ComponentModel.Composition.ExportAttribute utilisée par MEF.
L’interface ICodeIssueProvider nous force à implémenter trois méthodes :
IEnumerable<CodeIssue> GetIssues(IDocument document, CommonSyntaxNode node, CancellationToken cancellationToken = null);
IEnumerable<CodeIssue> GetIssues(IDocument document, CommonSyntaxToken token, CancellationToken cancellationToken = null);
IEnumerable<CodeIssue> GetIssues(IDocument document, CommonSyntaxTrivia trivia, CancellationToken cancellationToken = null);
Dans le cas de l’exemple, seule la première nous intéresse.
Cette méthode, comme son nom l’indique, nous permet de définir les erreurs que l’on souhaite générer.
public IEnumerable<CodeIssue> GetIssues(IDocument document, CommonSyntaxNode syntax, CancellationToken cancellationToken)
{var memberDeclaration = (MemberDeclarationSyntax)syntax;
if (!memberDeclaration.SupportsAccessibility() || memberDeclaration.HasAccessibilityKeyword())
{return null;
}
var semanticModel = document.GetSemanticModel(cancellationToken);
var symbol = (Symbol)semanticModel.GetDeclaredSymbol(memberDeclaration);
if (symbol == null)
{return null;
}
var issueSpan = memberDeclaration.GetIssueSpan();
if (issueSpan == null)
{return null;
}
var accessibility = symbol.DeclaredAccessibility;
var issueDescription = string.Format("Member should specify '{0}' accessibility", SyntaxFacts.GetText(accessibility));return new[]
{ new CodeIssue(CodeIssue.Severity.Warning, issueSpan.Value, issueDescription, new[]
{new CodeAction(editFactory, document, memberDeclaration, accessibility)
})
};
}
Dans l’attribut ExportSyntaxNodeCodeIssueProvider nous a permis de définir sur quel type de nœud l’erreur pouvait s’appliquer. En l’occurrence, des MemberDeclarationSyntax.
De ce fait, il est possible de directement caster le paramètre de type CommonSyntaxNode.
Ensuite, il faut s’assurer que le nœud supporte la notion de visibilité (ce n’est pas le cas des membres dans les interfaces, des namespaces et des destructeurs) et n’en ait pas de défini.
Pour cela, il suffit d’utiliser la propriété Modifiers qui est présente sur les BaseTypeDeclarationSyntax, les MethodDeclarationSyntax, les PropertyDeclarationSyntax et les FieldDeclarationSyntax.
Soit dit en passant, on pourra regretter le manque d’une interface commune.
La propriété Modifiers retourne un SyntaxTokenList qui possède une méthode Any prenant en paramètre un SyntaxKind. Tous les mots clés du langage sont présents dans l’enum SyntaxKind. De ce fait, il est possible de retrouver si notre SynatxTokenList contient le mot clé public ou protected ou internal ou private.
private static bool HasAccessibilityKeyword(this SyntaxTokenList modifierList)
{return modifierList.Any(SyntaxKind.PublicKeyword) ||
modifierList.Any(SyntaxKind.InternalKeyword) ||
modifierList.Any(SyntaxKind.ProtectedKeyword) ||
modifierList.Any(SyntaxKind.PrivateKeyword);
}
Une fois qu’on a déterminé que le membre peut contenir une visibilité et n’en a pas, il est possible de déterminer quelle est la visibilité implicite de l’élément. Pour cela, le plus fiable consiste à passer par le symbol associé.
var symbol = (Symbol)semanticModel.GetDeclaredSymbol(memberDeclaration);
var accessibility = symbol.DeclaredAccessibility;
Ensuite, il faut souligner le nom de la classe / méthode / propriété / champ qui n’a pas défini de visibilité. De la même manière qu’avec la propriété Modifiers, chacune des classes qui nous intéressent possède une propriété Identifier dont on peut déterminer la position dans le fichier avec la propriété Span.
Enfin, nous allons retourner une nouvelle instance de CodeIssue en lui précisant une action qui permettra de résoudre le problème.
return new[]
{ new CodeIssue(CodeIssue.Severity.Warning, issueSpan.Value, issueDescription, new[]
{new CodeAction(editFactory, document, memberDeclaration, accessibility)
})
};
La classe CodeAction implémente l’interface Roslyn.Services.Editor.ICodeAction qui contient une méthode GetEdit
ICodeActionEdit GetEdit(CancellationToken cancellationToken = null)
Dans l’implémentation de cette méthode, nous allons rajouter la visibilité. Pour cela, il suffit de rajouter un nouveau SyntaxToken avec la visibilité dans la propriété Modifiers des nœuds correspondant.
Le seul problème, c’est que les nœuds de l’arbre syntaxique (et par extension l’arbre complet) sont immutables. Il n’est donc pas possible de rajouter notre visibilité sans redéfinir le nœud et tous ces nœuds parents.
Pour modifier un nœud de type MethodDeclarationSyntax, il va donc falloir en redéfinir un.
private static MemberDeclarationSyntax PrependAccessibility(MethodDeclarationSyntax methodDeclaration, Accessibility accessibility)
{return Syntax.MethodDeclaration(
methodDeclaration.Attributes,
Syntax.TokenList(GetNewModifiers(accessibility, methodDeclaration.Modifiers)),
methodDeclaration.ReturnType,
methodDeclaration.ExplicitInterfaceSpecifierOpt,
methodDeclaration.Identifier,
methodDeclaration.TypeParameterListOpt,
methodDeclaration.ParameterList,
methodDeclaration.ConstraintClauses,
methodDeclaration.BodyOpt,
methodDeclaration.SemicolonTokenOpt);
}
On pourra regretter de ne pas avoir d’interface un peu plus fluent pour faire cela. On pourrait effectivement très facilement imaginer une méthode d’extension qui prenne en paramètre la MethodeDeclarationSyntax initiale et l’ensemble des autres paramètres de manière optionnelle.
Ensuite, il est possible d’utiliser la méthode ReplaceNode pour éviter de remplacer tous les nœuds parents.
var syntaxTree = (SyntaxTree)document.GetSyntaxTree();
var newRoot = syntaxTree.Root.ReplaceNode(memberDeclaration, newMemberDeclaration);
Enfin, il faut utiliser la ligne suivante pour appliquer la modification.
return editFactory.CreateTreeTransformEdit(document.Project.Solution, syntaxTree, newRoot);
A noter que editFactory a été importé avec MEF.
private readonly ICodeActionEditFactory editFactory;
[ImportingConstructor]
public CodeIssueProvider(ICodeActionEditFactory editFactory)
{this.editFactory = editFactory;
}
Nous venons de voir comment utiliser Roslyn pour générer et corriger automatiquement des erreurs.
Nous allons maintenant nous pencher sur le projet StyleCopCS.
Ce projet, comme le précédent est de type Code Issue.
Il exporte plusieurs CodeIssueProvider. Nous allons nous concentrer sur celui du dossier InsertBraces.
Si on suit les règles Style Cop, les instructions de type block (if / else / while / …) doivent être suivi d’accolades même si celles-ci n’englobent qu’une seule instruction alors que dans ce cas, les accolades sont facultatives pour le compilateur.
Après avoir exécuté ce projet, nous allons créer un projet dans lequel nous allons taper le code suivant :
.png)
Comme dans l’exemple précédent, les erreurs sont surlignées et peuvent être automatiquement corrigées.
.png)
Il est également possible de fixer toutes les erreurs du même type
.png)
Enfin, il est possible d’appliquer ces corrections sur des blocks n’étant pas en erreur mais contenant des erreurs.
.png)
Dans ce dernier cas, le curseur était sur while.
Tout d’abord, comment avoir deux options de correction ?
Il aurait été possible de retourner plusieurs CodeIssues mais en l’occurrence une seule est retournée.
Les trois constructeurs de CodeIssue prennent plusieurs ICodeAction en paramètre.
public CodeIssue(CodeIssue.Severity severity, TextSpan textSpan, IEnumerable<ICodeAction> actions);
public CodeIssue(CodeIssue.Severity severity, TextSpan textSpan, string description, IEnumerable<ICodeAction> actions);
public CodeIssue(CodeIssue.Severity severity, TextSpan textSpan, string description, params ICodeAction[] actions);
La méthode GetIssues retourne, en cas d’erreur, un CodeIssue avec deux CodeActions.
return new[]
{new CodeIssue(
CodeIssue.Severity.Warning,
node.GetFirstToken().Span,
"SA1503: The body of the statement must be wrapped in opening and closing curly brackets.",
codeAction,
new CodeAction("Fix all occurrences", editFactory, document, tree => new Rewriter(_ => true)))};
Comment déterminer l’absence de parenthèse en ayant sélectionné le while ?
Pour cela, il faut parcourir les enfants du nœud sélectionné. La meilleure façon de faire cela est de passer par le design pattern Visiteur.
var codeAction = new Visitor(editFactory, document, cancellationToken).Visit(syntaxNode);
Ce visiteur hérite de la classe Roslyn.Compilers.CSharp.SyntaxVisitor<TResult>. En l’occurrence, TResult est de type ICodeAction.
internal class Visitor : SyntaxVisitor<ICodeAction>
Ensuite, il suffit de surcharger les méthodes Visit qui nous intéressent.
protected override ICodeAction VisitIfStatement(IfStatementSyntax node)
{return node.NeedsBraces()
? CreateCodeAction(node)
: base.VisitIfStatement(node);
}
protected override ICodeAction VisitWhileStatement(WhileStatementSyntax node)
{return node.NeedsBraces()
? CreateCodeAction(node)
: base.VisitWhileStatement(node);
}
Il faut alors déterminer si l’instruction n’a pas d’accolade et si elle en a, tester ces enfants.
Pour tester ces enfants, il suffit de laisser le visiteur faire son travail.
Pour tester s’il manque des accolades, il faut regarder si le noeud n’est pas déjà dans le cas d’un block et si les accolades sont manquantes :
public static bool NeedsBraces(this WhileStatementSyntax node)
{return
!node.CloseParenToken.IsMissing &&
node.Statement.Kind != SyntaxKind.Block;
}
Ainsi, avec notre visiteur, nous sommes capables de trouver les erreurs (la première dans notre exemple) dans un block.
Ensuite, il faut créer le CodeAction. Pour cela, nous allons utiliser un Rewriter.
private ICodeAction CreateCodeAction(SyntaxNode node)
{ return new CodeAction("Insert missing braces", editFactory, document, tree => new Rewriter(current => current == node));}
La classe Rewriter hérite de la classe Roslyn.Compilers.CSharp.SyntaxRewriter. Cette classe hérite elle-même de SyntaxVisitor<SyntaxNode>.
L’idée du SyntaxRewriter est de parcourir les nœuds et d’en transformer certains.
Dans le cadre de l’exemple, le constructeur du Rewriter prend en paramètre un prédicat permettant de déterminer si le nœud est inclu dans ceux à corriger ou non. Ce prédicat est utile si on applique la correction sur un seul nœud.
Ensuite, la classe CodeAction va exécuter le Rewriter sur l’ensemble de l’arbre du document sélectionné dans la méthode GetEdit.
public ICodeActionEdit GetEdit(CancellationToken cancellationToken)
{var tree = (SyntaxTree)document.GetSyntaxTree(cancellationToken);
var rewriter = createRewriter(tree);
var result = rewriter.Visit(tree.Root);
return this.editFactory.CreateTreeTransformEdit(document.Project.Solution, tree, result);
}
La classe Rewriter va tester si le nœud est en erreur et si c’est le cas et que le prédicat est valable, elle va le corriger.
protected override SyntaxNode VisitIfStatement(IfStatementSyntax node)
{node = (IfStatementSyntax)base.VisitIfStatement(node);
if (!predicate(node) || !node.NeedsBraces())
{return node;
}
return CodeActionAnnotations.FormattingAnnotation.AddAnnotationTo(
Syntax.IfStatement(
node.IfKeyword,
node.OpenParenToken,
node.Condition,
node.CloseParenToken,
WrapStatementWithBlock(node.Statement),
node.ElseOpt));
}
Les annotations de formatage permettent d’indiquer à Roslyn qu’il doit les remettre en forme (faire les sauts de ligne et l’indentation qui vont bien).
Intéressons-nous maintenant à l’exemple ConvertToAutoPropertyCS.
Il permet de transformer une propriété encapsulant un champ en une propriété automatique (en mode get ; set ;) et supprime le champ devenu inutile.
Cet exemple n’est pas de type « Code Issue » mais de type « Code Refactoring ».
La logique est exactement la même, sauf qu’au lieu d’avoir un ICodeIssueProvider, nous utiliserons un ICodeRefactoringProvider.
[ExportCodeRefactoringProvider("ConvertToAutoPropertyCS", LanguageNames.CSharp)]internal class CodeRefactoringProvider : ICodeRefactoringProvider
{Cette interface contient une méthode GetRefactoring.
CodeRefactoring GetRefactoring(IDocument document, TextSpan textSpan, CancellationToken cancellationToken = null)
Cette méthode commence par chercher le nœud sélectionné.
var syntaxTree = document.GetSyntaxTree(cancellationToken);
var token = syntaxTree.Root.FindToken(textSpan.Start);
Ensuite, elle cherche si l’élément sélectionné appartient à un élément de type PropertyDeclarationSyntax et si celui-ci a bien implémenté un get et un set.
var propertyDeclaration = token.Parent.FirstAncestorOrSelf<PropertyDeclarationSyntax>();
// Refactor only properties with both a getter and a setter.
if (propertyDeclaration == null || !HasBothAccessors(propertyDeclaration))
{
return null;
}
Enfin, elle retourne un CodeRefactoring prenant en paramètre un CodeAction.
return new CodeRefactoring(
new[] { new CodeAction(editFactory, document, propertyDeclaration) },propertyDeclaration.Identifier.Span);
La méthode GetEdit du CodeAction commence par récupérer le SyntaxTree du document ainsi que le SemanticModel.
var tree = (SyntaxTree)document.GetSyntaxTree(cancellationToken);
var semanticModel = (SemanticModel)document.GetSemanticModel(cancellationToken);
Puis elle va récupérer le get de la propriété.
var getter = property.AccessorList.Accessors.FirstOrDefault(ad => ad.Kind == SyntaxKind.GetAccessorDeclaration);
Ensuite, on va rechercher la classe qui contient la propriété. Pour cela, on aurait pu rechercher dans les parents de la PropertyDeclarationSyntax. L’exemple, lui, utilise le SemanticModel
var containingType = semanticModel.GetDeclaredSymbol(property).ContainingType;
Ensuite, pour récupérer le champ, il faut dans un premier temps s’assurer que le get ne contient qu’un return (ce sera forcément le cas s’il n’y a qu’une seule instruction. Ensuite, il suffit de récupérer le nom du champ (en l’occurrence on passera par son symbole).
var statements = getter.BodyOpt.Statements;
if (statements.Count == 1)
{var returnStatement = statements.FirstOrDefault() as ReturnStatementSyntax;
if (returnStatement != null && returnStatement.ExpressionOpt != null)
{var semanticInfo = document.GetSemanticModel().GetSemanticInfo(returnStatement.ExpressionOpt);
var fieldSymbol = semanticInfo.Symbol as FieldSymbol;
if (fieldSymbol != null && fieldSymbol.OriginalDefinition.ContainingType == containingType)
{return fieldSymbol;
}
}
}
Enfin, il suffit d’utiliser un Rewriter pour effectuer les modifications sur le code.
var propertyRewriter = new PropertyRewriter(semanticModel, backingField, property);
var newRoot = propertyRewriter.Visit(tree.Root);
return editFactory.CreateTreeTransformEdit(document.Project.Solution, tree, newRoot);
Pour finir, nous allons étudier une particularité du projet ImplementNotifyPropertyChangedCS. Ce projet, en plus du vsixmanifest, intègre un vsct. L’idée ici n’est pas de faire un outil de refactoring mais d’intégrer une commande dans le menu contextuel de Visual Studio. Cependant, ce n’est pas ce qui m’intéresse dans le cadre de cet article.
Il y a quelque chose de très important avec les Rewriter, les instances des nœuds ne sont pas celles que l’on crées. Il est très important de savoir cela. Afin de retrouver un nœud dans un nouvel arbre syntaxique, il faut passer par une annotation.
Dans le cas de l’exemple, on applique une instance de Roslyn.Compilers.SyntaxAnnotation aux nœuds modifiés lors de l’implémentation de INotifyPropertyChanged.
Annotations.PropertyAnnotation.AddAnnotationTo(newProperty);
Ensuite, il est possible de récupérer les nœuds annotés comme ceci :
var annotatedProperties = newRoot.GetAnnotatedNodesAndTokens(Annotations.PropertyAnnotation)
.Select(p => p.AsNode())
.Cast<PropertyDeclarationSyntax>();
Nous avons vu dans cette partie comment développer un CodeIssueProvider, un CodeRefactoringProvider, comment mettre à jour un nœud dans un arbre syntaxique, comment utiliser les classes SyntaxVisitor<TResult> et SyntaxRewriter et comment récupérer des nœuds préalablement annotés.
L’analyse de code peut avoir plusieurs buts :
Pour la génération de code, je vous renvoie vers un autre article que j’ai écrit dans lequel j’utilise Roslyn dans un T4 pour générer du code.
Comme avec la compilation à la volée, il est possible de compiler des projets avec Roslyn. La partie intéressante repose sur le fait qu’il est possible de préalablement modifier l’arbre syntaxique de façon à compiler un code différent du code écrit. C’est notamment très utile dans l’AOP. Cependant, cela présente un problème sur laquelle l’équipe Roslyn travaille : il n’est dès lors plus possible de débuguer le code vu que les pdbs ne correspondent pas au code source.
Il faut également noter que la compilation avec Roslyn ne gère pas tout. Par exemple, les évènements ne sont pas supportés. De même, les instructions yield return / yield break ne le sont pas non plus. Vous trouverez plus d’informations sur les éléments non supportés ici.
Roslyn est encore en CTP mais est déjà très prometteur et ouvre de nouvelles possibilités pour les développeurs,
Parmi celles-ci, les principales, de mon point de vue, sont l’extensibilité de Visual Studio voire des langages C# ou VB.NET (en modifiant le compilateur).
Matthieu MEZIL