Junho de 2016

Volume 31 - Número 6

Plataforma do Compilador do .NET - Geração de código independente da linguagem com o Roslyn

Por Alessandro Del Del

A base de código do Roslyn fornece APIs avançadas as quais você pode aproveitar para executar uma análise detalhada de seu código-fonte. Por exemplo, analisadores e as refatorações de código podem percorrer um trecho do código-fonte e substituir um ou mais nós de sintaxe por um novo código gerado com as APIs do Roslyn. Uma forma comum de executar a geração de código é por meio da classe SyntaxFactory, que expõe métodos de fábrica a fim de gerar nós de sintaxe de uma maneira que os compiladores entendam. A classe SyntaxFactory é realmente muito poderosa, pois permite a geração de qualquer elemento de sintaxe possível, mas há duas implementações diferentes para a SyntaxFactory: Microsoft.CodeAnalysis.CSharp.SyntaxFactory e Microsoft.Code­Analysis.VisualBasic.SyntaxFactory. Isso é muito importante se você quiser escrever um analisador com uma correção de código que sirva tanto para C# quanto para Visual Basic. Você precisa escrever dois analisadores diferentes, um para C# e outro para Visual Basic, usando as duas implementações da SyntaxFactory, cada um com uma abordagem diferente, pois lidam com algumas construções de forma diferente. Isso, provavelmente, significa perda de tempo escrevendo o analisador duas vezes e mais dificuldade de manutenção dos dois. Felizmente, a API do Roslyn também fornece o Microsoft.CodeAnalysis.Editing.SyntaxGenerator, que permite a geração de código independente da linguagem. Em outras palavras, com a Syntax­Generator você pode escrever sua lógica de geração de código uma vez e direcioná-la para C# e Visual Basic. Neste artigo, vou mostrar como executar a geração de código independente da linguagem com a SyntaxGenerator, e darei algumas dicas sobre as APIs de Espaço de Trabalho do Roslyn.

Começando com o código

Vamos começar com um código-fonte que será gerado usando a SyntaxGenerator. Considere a classe simples, Person, que implementa a interface ICloneable em C# (Figura 1) e no Visual Basic (Figura 2).

Figura 1 Uma classe simples, Person, em C#

public abstract class Person : ICloneable
{
  // Not using auto-props is intentional for demo purposes
  private string _lastName;
  public string LastName
  {
    get
    {
      return _lastName;
    }
    set
    {
      _lastName = value;
    }
  }
  private string _firstName;
  public string FirstName
  {
    get
    {
      return _firstName;
    }
    set
    {
      _firstName = value;
    }
  }
  public Person(string LastName, string FirstName)
  {
    _lastName = LastName;
    _firstName = FirstName;
  }
  public virtual object Clone()
  {
    return MemberwiseClone();
  }
}

Figura 2 Uma classe simples, Person, no Visual Basic

Public MustInherit Class Person
  Implements ICloneable
  'Not using auto-props is intentional for demo purposes
  Private _lastName As String
  Private _firstName As String
  Public Property LastName As String
    Get
      Return _lastName
    End Get
    Set(value As String)
      _lastName = value
    End Set
  End Property
  Public Property FirstName As String
    Get
      Return _firstName
    End Get
    Set(value As String)
      _firstName = value
    End Set
  End Property
  Public Sub New(LastName As String, FirstName As String)
    _lastName = LastName
    _firstName = FirstName
  End Sub
  Public Overridable Function Clone() As Object Implements ICloneable.Clone
    Return MemberwiseClone()
  End Function
End Class

Provavelmente você vai dizer que declarar propriedades implementadas automaticamente teria o mesmo efeito e manter o código mais limpo nesse caso específico, mas você verá depois o motivo de eu estar usando a forma expandida.

Esta implementação da classe Person é bastante simples, mas ela contém uma boa quantidade de elementos de sintaxe, o que a torna útil para tentar entender como executar a geração de código com a Syntax­Generator. Vamos gerar essa classe com o Roslyn.

Como criar uma ferramenta de análise de código

A primeira coisa que precisamos fazer é criar um novo projeto no Visual Studio 2015 com referências às bibliotecas do Roslyn. Como este artigo é mais geral, em vez de criar um analisador ou uma refatoração, vou escolher outro modelo de projeto disponível no SDK da plataforma do Compilador do .NET, Ferramenta de Análise de Código Autônoma, disponível no nó Extensibilidade da caixa de diálogo Novo Projeto (consulte a Figura 3).

O modelo de projeto Ferramenta de Análise de Código Autônoma
Figura 3 O modelo de projeto Ferramenta de Análise de Código Autônoma

Este modelo de projeto gera um aplicativo de console e adiciona automaticamente os pacotes NuGet apropriados para as APIs do Roslyn, servindo para a linguagem que você escolher. Como a ideia é servir para C# e Visual Basic, a primeira coisa a ser feita é adicionar os pacotes NuGet para a segunda linguagem. Por exemplo, se você criar inicialmente um projeto em C#, será necessário baixar e instalar as seguintes bibliotecas do Visual Basic a partir do NuGet:

  • Microsoft.CodeAnalysis.VisualBasic.dll
  • Microsoft.CodeAnalysis.VisualBasic.Workspaces.dll
  • Microsoft.CodeAnalysis.VisualBasic.Workspaces.Common.dll

Você pode instalar apenas a última do NuGet, e isso resolverá automaticamente as dependências das outras bibliotecas necessárias. A resolução de dependências será importante sempre que você planejar usar a classe SyntaxGenerator, não importa o modelo de projeto que você esteja usando. Se você esquecer isso, obterá exceções no tempo de execução.

Conheça a SyntaxGenerator e as APIs de Espaço de Trabalho

A classe SyntaxGenerator expõe um método estático chamado GetGenerator, que retorna uma instância da SyntaxGenerator. Use a instância retornada para executar a geração de código. GetGenerator tem as três sobrecargas a seguir:

public static SyntaxGenerator GetGenerator(Document document)
public static SyntaxGenerator GetGenerator(Project project)
public static SyntaxGenerator GetGenerator(Workspace workspace, string language)

As duas primeiras sobrecargas trabalham em uma classe Document e uma classe Project, respectivamente. A classe Document representa um arquivo de código em um projeto, enquanto a classe Project representa um projeto do Visual Studio como um todo. Essas sobrecargas detectam automaticamente a linguagem (C# ou Visual Basic) que a classe Document ou Project usa. Document, Project e Solution (uma classe adicional que representa uma solução .sln do Visual Studio) fazem parte de um Espaço de Trabalho, que fornece uma maneira gerenciada de interagir com tudo que compõe uma solução MSBuild, com projetos, arquivos de código, metadados e objetos. As APIs de Espaço de Trabalho expõem várias classes que você pode usar para gerenciar os espaços de trabalho, como a classe MSBuildWorkspace, que permite a você trabalhar em uma solução .sln, ou a classe AdhocWorkspace, que é bastante útil quando você não está trabalhando em uma solução MSBuild existente, mas quer um espaço de trabalho na memória que represente uma. No caso dos analisadores e da refatoração de código, você já tem um espaço de trabalho MSBuild que permite a você trabalhar em arquivos de código usando instâncias das classes Document, Project e Solution. No exemplo de projeto atual, não há espaço de trabalho, então vamos criar um usando a terceira sobrecarga de SyntaxGenerator. Para obter um novo espaço de trabalho vazio, você pode usar a classe AdhocWorkspace:

// Get a workspace
var workspace = new AdhocWorkspace();

Agora você pode obter uma instância de SyntaxGenerator, passando a instância workspace e a linguagem desejada como argumentos:

// Get the SyntaxGenerator for the specified language
var generator = SyntaxGenerator.GetGenerator(workspace, LanguageNames.CSharp);

O nome da linguagem pode ser em CSharp ou VisualBasic, ambos constantes da classe LanguageNames. Vamos começar com C#; depois você verá como alterar o nome da linguagem para VisualBasic. Agora você tem todas as ferramentas necessárias e está pronto para gerar nós de sintaxe.

Como gerar nós de sintaxe

A classe SyntaxGenerator expõe métodos de fábrica da instância que geram nós de sintaxe apropriados de uma forma compatível com a gramática e a semântica de C# e Visual Basic. Por exemplo, métodos com nomes que terminam com o sufixo Expression geram expressões; métodos com nomes que terminam com o sufixo Statement geram instruções; métodos com nomes que terminam com o sufixo Declaration geram declarações. Para cada categoria, há métodos especializados que geram nós de sintaxe específicos. Por exemplo, MethodDeclaration gera um bloco de método, PropertyDeclaration gera uma propriedade, FieldDeclaration gera um campo etc (e, como de costume, IntelliSense é seu melhor amigo). A peculiaridade desses métodos é que cada um retorna SyntaxNode, em vez de um tipo especializado derivado de SyntaxNode, como acontece com a classe SyntaxFactory. Isso proporciona muita flexibilidade, especialmente ao gerar nós complexos.

Com base no exemplo de classe Person, a primeira coisa a ser gerada é uma diretiva using/Imports para o namespace System, que expõe a interface ICloneable. Isso pode ser realizado com o método NamespaceImportDeclaration, da seguinte maneira:

// Create using/Imports directives
var usingDirectives = generator.NamespaceImportDeclaration("System");

Esse método usa um argumento de cadeia de caracteres que representa o namespace que você deseja importar. Vamos seguir em frente e declarar dois campos, o que é feito pelo método FieldDeclaration:

// Generate two private fields
var lastNameField = generator.FieldDeclaration("_lastName",
  generator.TypeExpression(SpecialType.System_String),
  Accessibility.Private);
var firstNameField = generator.FieldDeclaration("_firstName",
  generator.TypeExpression(SpecialType.System_String),
  Accessibility.Private);

FieldDeclaration usa o nome do campo, o tipo do campo e o nível de acessibilidade como argumentos. Para fornecer o tipo apropriado, invoque o método TypeExpression, que usa um valor da enumeração SpecialType, neste caso System_String (não se esqueça de usar IntelliSense para descobrir outros valores). O nível de acessibilidade é definido com um valor a partir da enumeração Accessibility. Ao invocar métodos de SyntaxGenerator, é bastante comum aninhar invocações a outros métodos a partir da mesma classe, como é o caso com TypeExpression. O próximo passo é gerar duas propriedades, o que pode ser feito invocando o método PropertyDeclaration, exibido na Figura 4.

Figura 4 Geração de duas propriedades usando o método PropertyDeclaration

// Generate two properties with explicit get/set
var lastNameProperty = generator.PropertyDeclaration("LastName",
  generator.TypeExpression(SpecialType.System_String), Accessibility.Public,
  getAccessorStatements:new SyntaxNode[]
  { generator.ReturnStatement(generator.IdentifierName("_lastName")) },
  setAccessorStatements:new SyntaxNode[]
  { generator.AssignmentStatement(generator.IdentifierName("_lastName"),
  generator.IdentifierName("value"))});
var firstNameProperty = generator.PropertyDeclaration("FirstName",
  generator.TypeExpression(SpecialType.System_String),
  Accessibility.Public,
  getAccessorStatements: new SyntaxNode[]
  { generator.ReturnStatement(generator.IdentifierName("_firstName")) },
  setAccessorStatements: new SyntaxNode[]
  { generator.AssignmentStatement(generator.IdentifierName("_firstName"),
  generator.IdentifierName("value")) });

Como você pode ver, é mais complexo gerar um nó de sintaxe para a propriedade. Aqui você ainda passa uma cadeia de caracteres com o nome da propriedade, depois um TypeExpression para o tipo de propriedade e depois o nível de acessibilidade. Com uma propriedade você também precisa fornecer os acessadores Get e Set, especialmente para as situações nas quais é necessário executar algum código diferente, que não seja para configurar ou retornar o valor da propriedade (por exemplo, acionar o evento OnPropertyChanged ao implementar a interface INotifyPropertyChanged). Os acessadores Get e Set são representados por uma matriz de objetos SyntaxNode. No Get, normalmente você retorna o valor da propriedade, então aqui o código invoca o método ReturnStatement, que representa a instrução return mais o valor ou objeto que ela retorna. Nesse caso, o valor retornado é um identificador de campo. Um nó de sintaxe para um identificador é obtido por meio da invocação do método IdentifierName, que usa um argumento do tipo string e ainda retorna SyntaxNode. Os acessadores Set, pelo contrário, armazenam o valor da propriedade em um campo por meio de uma atribuição. As atribuições são representadas pelo método AssignmentStatement, que usa dois argumentos, os lados direito e esquerdo da atribuição. No caso atual, a atribuição ocorre entre dois identificadores, então o código invoca IdentifierName duas vezes, uma para o lado esquerdo da atribuição (o nome do campo) e a outra para o lado direito (o valor da propriedade). Como o valor da propriedade é representado pelo identificador do valor em C# e Visual Basic, ele pode ser codificado.

A próxima etapa é a geração de código para o método Clone, exigido pela implementação da interface ICloneable. De modo geral, um método é composto pela declaração, que inclui a assinatura e os delimitadores de bloco, além de e diversas instruções, que compõem o corpo do método. No exemplo atual, Clone também deve implementar o método ICloneable.Clone. Por esse motivo, uma abordagem conveniente é dividir a geração do código do método em três nós de sintaxe menores. O primeiro nó de sintaxe é o corpo do método, que se parece com o seguinte:

// Generate the method body for the Clone method
var cloneMethodBody = generator.ReturnStatement(generator.
  InvocationExpression(generator.IdentifierName("MemberwiseClone")));

Nesse caso, o método Clone retorna os resultados da invocação do método MemberwiseClone, herdado de System.Object. Por esse motivo, o corpo do método é apenas uma invocação de ReturnStatement, que você conheceu anteriormente. Aqui, o argumento de ReturnStatement é uma invocação do método InvocationExpression, que representa uma invocação de método e cujo parâmetro é um identificador que representa o nome do método invocado. Como o argumento InvocationExpression é do tipo SyntaxNode, uma forma conveniente de fornecer o identificador é usar o método IdentifierName, passando a cadeia de caracteres que representa o identificador do método a ser invocado. Se você tivesse um método com um corpo de método mais complexos, seria necessário gerar uma matriz do tipo SyntaxNode, com cada nó representando algum código no corpo do método.

O próximo passo é gerar a declaração do método Clone, que é obtida da seguinte forma:

// Generate the Clone method declaration
var cloneMethoDeclaration = generator.MethodDeclaration("Clone", null,
  null,null,
  Accessibility.Public,
  DeclarationModifiers.Virtual,
  new SyntaxNode[] { cloneMethodBody } );

Você gera um método com o método MethodDeclaration. Isso usa diversos argumentos, como:

  • o nome do método, do tipo String
  • os parâmetros do método, do tipo IEnumerable<SyntaxNode> (nulo neste caso)
  • os parâmetros de tipo para métodos genéricos, do tipo IEnumerable<SyntaxNode> (nulo neste caso)
  • o tipo de retorno, o tipo de SyntaxNode (nulo neste caso)
  • o nível de acessibilidade, com um valor da enumeração Accessibility.
  • os modificadores de declaração, com um ou mais valores da enumeração DeclarationModifiers; neste caso, o modificador é virtual (substituível no Visual Basic)
  • as instruções para o corpo do método, do tipo SyntaxNode; neste caso, a matriz contém um elemento, que é a instrução return definida anteriormente

Em breve você verá um exemplo de como adicionar parâmetros de método com o método mais especializado ConstructorDeclaration. O método Clone deve implementar sua contraparte a partir da interface ICloneable, então é preciso lidar com isso. O que você precisa saber é um nó de sintaxe que representa o nome da interface e que também será útil quando a implementação da interface for adicionada à classe Person. Isso pode ser realizado por meio da invocação do método IdentifierName, que retorna um nome próprio da cadeia de caracteres especificada:

// Generate a SyntaxNode for the interface's name you want to implement
var ICloneableInterfaceType = generator.IdentifierName("ICloneable");

Se você quisesse importar o nome totalmente qualificado, System.ICloneable, seria necessário usar DottedName em vez de IdentifierName a fim de gerar um nome qualificado apropriado, mas no exemplo atual um NamespaceImportDeclaration para System já foi adicionado. Neste ponto, você pode juntar tudo. SyntaxGenerator tem os métodos AsPublicInterfaceImplementation e AsPrivateInterfaceImplementation que você usa para informar ao compilador que uma definição de método está implementando uma interface, como na maneira a seguir:

// Explicit ICloneable.Clone implemenation
var cloneMethodWithInterfaceType = generator.
  AsPublicInterfaceImplementation(cloneMethoDeclaration,
  ICloneableInterfaceType);

Isso é particularmente importante com o Visual Basic, que exige explicitamente a cláusula Implements. AsPublicInterfaceImplementation é o equivalente à implementação de interface implícita em C#, enquanto AsPrivateInterfaceImplementation é o equivalente à implementação de interface explícita. Ambas funcionam com métodos, propriedades e indexadores.

O próximo passo é sobre a geração do construtor, que é realizada por meio do método ConstructorDeclaration. Assim como ocorre com o método Clone, a definição do construtor deve ser dividida em pedaços menores para facilitar a compreensão e limpar o código. Como você deve se lembrar da Figura 1 e da Figura 2, o construtor usa dois parâmetros do tipo string, que são necessários para a inicialização da propriedade. Então, é uma boa ideia gerar o nó de sintaxe para os dois parâmetros primeiro:

// Generate parameters for the class' constructor
var constructorParameters = new SyntaxNode[] {
  generator.ParameterDeclaration("LastName",
  generator.TypeExpression(SpecialType.System_String)),
  generator.ParameterDeclaration("FirstName",
  generator.TypeExpression(SpecialType.System_String)) };

Cada parâmetro é gerado com o método ParameterDeclaration, que usa uma cadeia de caracteres representando o nome do parâmetro, e uma expressão que representa o tipo do parâmetro. Os dois parâmetros são do tipo String, então o código usa simplesmente o método TypeExpression, como você já aprendeu. O motivo para agrupar os dois parâmetros em um SyntaxNode é que o ConstructorDeclaration deseja um objeto desse tipo para representar parâmetros.

Agora você precisa construir o corpo do método, que aproveita o método AssignmentStatement que você viu anteriormente, da seguinte maneira:

// Generate the constructor's method body
var constructorBody = new SyntaxNode[] {
  generator.AssignmentStatement(generator.IdentifierName("_lastName"),
  generator.IdentifierName("LastName")),
  generator.AssignmentStatement(generator.IdentifierName("_firstName"),
  generator.IdentifierName("FirstName"))};

Nesse caso, há duas instruções, ambas agrupadas em um objeto Syntax­Node. Por fim, você pode gerar o construtor, agrupando os parâmetros e o corpo do método:

// Generate the class' constructor
var constructor = generator.ConstructorDeclaration("Person",
  constructorParameters, Accessibility.Public,
  statements:constructorBody);

ConstructorDeclaration é parecido com MethodDeclaration, mas foi especificamente projetado para gerar um método .ctor em C# e um método Sub New no Visual Basic.

Como gerar um CompilationUnit

Até agora você viu como gerar um código para cada membro da classe Person. Agora você precisa juntar esses membros e gerar um SyntaxNode apropriado para a classe. Os membros da classe devem ser fornecidos na forma de um SyntaxNode, e o seguinte demonstra como juntar todos os membros criados anteriormente:

// An array of SyntaxNode as the class members
var members = new SyntaxNode[] { lastNameField,
  firstNameField, lastNameProperty, firstNameProperty,
  cloneMethodWithInterfaceType, constructor };

Agora você pode finalmente gerar a classe Person, aproveitando o método ClassDeclaration da seguinte maneira:

// Generate the class
var classDefinition = generator.ClassDeclaration(
  "Person", typeParameters: null,
  accessibility: Accessibility.Public,
  modifiers: DeclarationModifiers.Abstract,
  baseType: null,
  interfaceTypes: new SyntaxNode[] { ICloneableInterfaceType },
  members: members);

Assim como ocorre com outros tipos de declarações, esse método exige a especificação do nome, do tipo genérico (nulo neste caso), do nível de acessibilidade, do modificador (Abstrato neste caso, ou MustInherit no Visual Basic), tipos base (nulo neste caso) e as interfaces implementadas (neste caso um SyntaxNode contendo o nome da interface criado anteriormente como um nó de sintaxe). Convém também encapsular a classe em um namespace. SyntaxGenerator inclui o método NamespaceDeclaration, que aceita o nome do namespace e o SyntaxNode que ele contém. É possível usá-lo assim:

// Declare a namespace
var namespaceDeclaration = generator.NamespaceDeclaration("MyTypes", classDefinition);

Os compiladores já sabem como lidar com o nó de sintaxe gerado para o namespace completo e os membros aninhados, e como executar a análise do código sobre a sintaxe, mas às vezes você precisa retornar esse resultado na forma de CompilationUnit, um tipo que representa um arquivo de código. Isso é bem típico com analisadores e refatorações de código. Este é o código para retornar um CompilationUnit:

// Get a CompilationUnit (code file) for the generated code
var newNode = generator.CompilationUnit(usingDirectives, namespaceDeclaration).
  NormalizeWhitespace();

Este método aceita uma ou mais instâncias de SyntaxNode como o argumento.

A saída em C# e Visual Basic

Depois de todo esse trabalho, você está pronto para ver o resultado. A Figura 5 mostra o código C# gerado para a classe Person.

O código em C# gerado pelo Roslyn para a classe Person
Figura 5 O código em C# gerado pelo Roslyn para a classe Person

Agora, basta alterar a linguagem para VisualBasic na linha do código para criar um novo AdhocWorkspace:

generator = SyntaxGenerator.GetGenerator(workspace, LanguageNames.VisualBasic);

Se você executar o código novamente, receberá uma definição de classe de Visual Basic, conforme mostra a Figura 6.

O código em Visual Basic gerado pelo Roslyn para a classe Person
Figura 6 O código em Visual Basic gerado pelo Roslyn para a classe Person

O principal aqui é que com SyntaxGenerator você escreveu o código uma vez e foi capaz de gerar um código para C# e Visual Basic, e com ele as APIs de análise do Roslyn podem funcionar. Depois de concluir, não se esqueça de invocar o método Dispose sobre a instância AdhocWorkspace, ou simplesmente envolva seu código dentro de uma instrução using. Como ninguém é perfeito e o código gerado pode conter erros, você também pode verificar se há algum diagnóstico no código com a propriedade ContainsDiagnostics e obter informações detalhadas sobre problemas com o código por meio do método GetDiagnostics.

Analisadores e refatoração independentes da linguagem

Você pode usar as APIs do Roslyn e a classe SyntaxGenerator sempre que precisar executar análises avançadas no código-fonte, mas essa abordagem também é bastante útil com analisadores e refatorações de código. Na verdade, os analisadores, correções de código e refatorações têm os atributos DiagnosticAnalyzer, ExportCodeFixProvider e ExportCodeRefactoringProvider, respectivamente, e cada um aceita as linguagens com suporte primária e secundária. Ao usar SyntaxGenerator em vez de SyntaxFactory, você pode atender simultaneamente ao C# e ao Visual Basic.

Conclusão

A classe SyntaxGenerator do namespace Microsoft.CodeAnalysis.Editing fornece uma maneira independente da linguagem de geração de nós de sintaxe, atendendo ao C# e ao Visual Basic com uma base de código. Com essa classe poderosa, você pode gerar qualquer elemento de sintaxe possível de uma forma que seja compatível com os dois compiladores, economizando tempo e melhorando a capacidade de manutenção do código.


Alessandro Del Soleé Microsoft MVP desde 2008. Premiado cinco vezes como MVP of the Year, é autor de muitos livros, eBooks, vídeos instrutivos e artigos sobre o desenvolvimento em .NET com o Visual Studio. Del Sole trabalha como desenvolvedor especialista em soluções para a Brain-Sys, com foco em desenvolvimento, treinamento e consultoria em .NET. Você pode segui-lo no Twitter: @progalex.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: Anthony D. Green e Matt Warren
Anthony D. Green é o gerente de programa para Visual Basic. Anthony também trabalhou nessa coisa de Roslyn durante 5 anos. Ele é de Chicago e você pode encontrá-lo no Twitter @ThatVBGuy