Compiladores

Como o projeto de compilador de próxima geração da Microsoft pode melhorar o seu código

Jason Bock

Baixar o código de exemplo

Eu acredito que o desejo de qualquer desenvolvedor seja escrever código bom. Ninguém quer criar sistemas cheios de bugs e difíceis de manter que exigem horas infinitas para se adicionar recursos ou corrigir problemas. Eu já participei de projetos nos quais sentia que estava em um estado caótico constante, e isso não foi nem um pouco divertido. Inúmeras horas são dedicadas a uma base de código que mal dá para entender, devido a abordagens inconsistentes. Eu gosto de participar de projetos nos quais as camadas são bem-definidas, os testes de unidade são frequentes e os servidores de compilação estão em constante execução para garantir o bom funcionamento de todas as partes. Projetos como esses normalmente têm um conjunto de diretrizes e padrões que os desenvolvedores podem seguir.

Eu já vi equipes desenvolverem diretrizes como essas. Talvez os desenvolvedores devam evitar chamar determinados métodos em seus códigos porque foram considerados problemáticos. Ou talvez eles queiram garantir que o código siga os mesmos padrões em determinadas situações. Por exemplo, os desenvolvedores participantes de projetos podem concordar com padrões como os mostrados abaixo:

  • Valores de DateTime locais não devem ser usados. Todos os valores de DateTime devem estar no sistema UTC (Universal Time Coordinate).
  • O método Parse encontrado em tipos de valor (como int.Parse) deve ser evitado; int.TryParse deve ser usado como alternativa.
  • Todas as classes de entidades criadas devem dar suporte à igualdade — isto é, elas devem substituir Equals e GetHashCode e implementar os operadores == e !=, bem como a interface IEquatable<T>.

Tenho certeza de que você já viu regras como essas em um documento de padrões. Manter a consistência é uma boa prática, e se todos seguirem as mesmas regras, o código fica mais fácil de se manter. O truque consiste em divulgar rapidamente esse conhecimento para todos os desenvolvedores da equipe de uma forma reutilizável e eficaz.

As revisões de código são uma maneira de encontrar problemas potenciais. É comum que pessoas com uma perspectiva diferente sobre uma determinada implementação consigam ver questões que passam despercebidas pelo autor original. Ter mais alguém revisando o que você fez pode ser vantajoso, especialmente se o revisor não estiver familiarizado com o trabalho. Mesmo assim, ainda é fácil deixar passar algum problema durante o desenvolvimento. Além disso, as revisões de código são demoradas — os desenvolvedores têm que gastar muitas horas revisando o código e fazendo reuniões com outros desenvolvedores para comunicar os problemas encontrados. Eu quero um processo mais rápido. Eu quero saber imediatamente quando faço algo que está errado. Determinar os erros rapidamente é uma ótima forma de economizar tempo e dinheiro a longo prazo.

Existem ferramentas no Visual Studio, como a Análise de Código, que podem analisar seu código e informá-lo a respeito de problemas potenciais. O recurso Análise de Código tem uma série de regras predefinidas que podem identificar casos em que seu objeto não foi descartado, ou quando você tem argumentos de métodos não utilizados. Infelizmente, a Análise de Código não executa suas regras até que a compilação esteja concluída. E isso não é rápido o suficiente! Eu quero saber enquanto estou digitando se o meu código tem um erro de acordo com os meus padrões. Saber dos erros o mais rápido possível é muito bom. Eu posso economizar tempo (e, com isso, dinheiro) e evito prosseguir com um código que pode levar a diversos problemas no futuro. Para isso, eu preciso ser capaz de codificar minhas regras de forma que elas sejam executadas à medida que eu digito. E é aqui que entra o Microsoft “Roslyn” CTP.

O que é o Microsoft “Roslyn”?

Uma das melhores ferramentas que os desenvolvedores de .NET podem usar para analisar código é o compilador. Ele sabe analisar código em tokens e transformar esses tokens em algo significativo com base na sua posição dentro do código. O compilador faz isso emitindo um assembly ao disco como saída. Há uma grande quantidade de conhecimento obtido a duras penas reunido no pipeline de compilação que você adoraria usar. Mas, infelizmente, isso não é possível no mundo do .NET porque os compiladores de C# e Visual Basic não fornecem uma API para você acessar. Isso muda com o Roslyn. O Roslyn é um conjunto de APIs de compilador que fornece a você acesso completo a todas as etapas executadas pelo compilador. A Figura 1 mostra um diagrama das diferentes etapas no processo do compilador disponíveis agora com o Roslyn.

The Roslyn Compiler Pipeline
Figura 1 O pipeline do compilador do Roslyn

Embora o Roslyn ainda esteja no modo CTP (eu usei a versão de setembro de 2012 neste artigo), vale a pena dedicar algum tempo para investigar a funcionalidade disponível em seus assemblies e para aprender a usar o Roslyn. Uma boa forma de começar é analisando o recurso de script dele. Com o Roslyn, o código em C# e Visual Basic agora pode ser programado por script. Ou seja, existe um mecanismo de script disponível no Roslyn no qual você pode inserir trechos de código. Isso é feito pela classe ScriptEngine. Aqui está um exemplo que ilustra como esse mecanismo pode retornar o valor de DateTime atual:

class Program
{
  static void Main(string[] args)
  {
    var engine = new ScriptEngine();
    engine.ImportNamespace("System");
    var session = engine.CreateSession();
    Console.Out.WriteLine(session.Execute<string>(
      "DateTime.Now.ToString();"));
  }
}

Nesse código, o mecanismo está criando e importando o namespace System para que o Roslyn seja capaz de resolver o que DateTime significa. Depois de criada uma sessão, basta chamar Execute, e o Roslyn analisará o código específico. Se o código for analisado corretamente, ele será executado e retornará o resultado.

Tornar o C# uma linguagem de script é um conceito poderoso. Embora o Roslyn ainda esteja no modo CTP, as pessoas estão criando projetos e estruturas incríveis usando partes dele, como é o caso do scriptcs (scriptcs.net). No entanto, o que eu acho que é realmente o ponto forte do Roslyn é o fato de ele permitir a criação de extensões do Visual Studio que o avisam sobre os problemas enquanto você escreve o código. No trecho de código anterior, eu usei DateTime.Now. Se eu estivesse participando de um projeto que tivesse como padrão aquele primeiro item da lista com marcadores do início do artigo, eu estaria violando esse padrão. Eu explicarei como essa regra pode ser aplicada usando o Roslyn. Mas, antes de fazer isso, vou tratar da primeira etapa da compilação: analisar código para obter tokens.

Árvores de sintaxe

Quando o Roslyn analisa uma parte de um código, ele retorna uma árvore de sintaxe imutável. Essa árvore contém tudo sobre o código específico, incluindo espaços e tabulações. Mesmo que o código contenha erros, ele ainda tentará fornecer o máximo de informações que puder para você.

Isso está tudo muito bem, mas como podemos descobrir, em uma árvore, onde estão as informações pertinentes? Atualmente, a documentação sobre o Roslyn é bastante escassa, o que é compreensível por se tratar ainda de uma versão CTP. Você pode usar os fóruns do Roslyn para postar perguntas (bit.ly/16qNf7w) ou usar a tag #RoslynCTP em uma postagem no Twitter. Existe também um exemplo chamado SyntaxVisualizerExtension quando você instala os bits, que é uma extensão para o Visual Studio. À medida que você digita o código no IDE, o visualizador faz a atualização automaticamente com a versão atual da árvore.

Essa ferramenta é indispensável para você saber o que procura e como navegar pela árvore. Para usar .Now na classe DateTime, cheguei à conclusão de que eu precisava encontrar Member­AccessExpression (ou, para ser mais preciso, um objeto com base em MemberAccessExpression­Syntax), com o último valor de IdentifierName sendo igual a Now. Obviamente, isso se aplica ao caso simples em que você digita “var now = DateTime.Now;” — você poderia colocar “System.” na frente de DateTime ou usar “using DT = System.DateTime;”; além disso, talvez haja uma propriedade no sistema em uma classe diferente denominada Now. Todos esses casos devem ser processados corretamente.

Localizando e resolvendo problemas no código

Agora que sei o que procurar, preciso criar uma extensão do Visual Studio com base no Roslyn para rastrear o uso da propriedade DateTime.Now. Para isso, basta selecionar o modelo de problema de código sob a opção Roslyn no Visual Studio.

Depois de fazer isso, você terá um projeto que contém uma classe chamada CodeIssue­Pro­vider. Essa classe implementa a interface de ICodeIssue­Provider, embora você não tenha que implementar cada um dos seus quatro membros. Nesse caso, apenas os membros que trabalham com os tipos SyntaxNode são usados; os outros podem lançar NotImplementedException. Você implementa a propriedade SyntaxNodeTypes especificando os tipos de nós da sintaxe que deseja manipular com o método GetIssues correspondente. Como mencionado no exemplo anterior, os tipos MemberAccessExpressionSyntax são os que realmente importam. O trecho de código a seguir mostra como implementar SyntaxNodeTypes:

public IEnumerable<Type> SyntaxNodeTypes
{
  get
  {
    return new[] { typeof(MemberAccessExpressionSyntax) };
  }
}

Essa é uma otimização no Roslyn. Fazendo com que você especifique quais tipos deseja examinar mais detalhadamente, o Roslyn não precisa chamar o método GetIssues para cada tipo de sintaxe. Se o Roslyn não tivesse esse mecanismo de filtragem embutido e precisasse chamar o provedor de código para cada um dos nós da árvore, o desempenho seria sofrível.

Agora tudo o que nos resta é implementar Get­Issues de forma que ele reporte apenas o uso da propriedade Now. Como mencionado na seção anterior, você só quer localizar os casos em que Now foi usado em DateTime. Quando você utiliza tokens, não tem um monte de informações ao lado do texto. No entanto, o Roslyn fornece o que denominamos modelo semântico, que pode fornecer muito mais informações sobre o código sendo examinado. O código na Figura 2 demonstra como localizar os usos de DateTime.Now.

Figura 2 Localizando os usos de DateTime.Now

public IEnumerable<CodeIssue> GetIssues(
  IDocument document, CommonSyntaxNode node, 
  CancellationToken cancellationToken)
{
  var memberNode = node as MemberAccessExpressionSyntax;
  if (memberNode.OperatorToken.Kind == SyntaxKind.DotToken &&
    memberNode.Name.Identifier.ValueText == "Now")
  {
    var symbol = document.GetSemanticModel()
        .GetSymbolInfo(memberNode.Name).Symbol;
    if (symbol != null &&
      symbol.ContainingType.ToDisplayString() ==
        Values.ExpectedContainingDateTimeTypeDisplayString &&
      symbol.ContainingAssembly.ToDisplayString().Contains(
        Values.ExpectedContainingAssemblyDisplayString))
    {
      return new [] { new CodeIssue(CodeIssueKind.Error,
        memberNode.Name.Span,
        "Do not use DateTime.Now",
        new ChangeNowToUtcNowCodeAction(document, memberNode))};
    }
  }
  return null;
}

Você perceberá que o argumento cancellationToken não é usado aqui nem em nenhuma parte do projeto de exemplo que acompanha este artigo. Isso foi proposital, pois colocar código no exemplo que verifique constantemente o token para saber se o processamento deve ser interrompido pode se transformar em uma distração. Mas se você pretende criar extensões com base no Roslyn que possam ser usadas em produção imediatamente, não deixe de verificar o token com frequência e interromper o processamento caso o token esteja em estado cancelado.

Depois de determinar se a expressão de acesso do membro está tentando obter uma propriedade denominada Now, você poderá obter informações sobre o símbolo desse token. Isso pode ser feito obtendo o modelo semântico da árvore e, em seguida, obtendo uma referência a um objeto com base em ISymbol pela propriedade Symbol. Depois disso, tudo o que você precisa fazer é obter o tipo recipiente e verificar se o nome dele é System.DateTime e se o nome do assembly recipiente inclui mscorlib. Se esse for o caso, este é o problema pelo qual você estava procurando, e você poderá marcá-lo como um erro retornando um objeto CodeIssue.

Fizemos um bom progresso até agora, porque você verá uma linha ondulada de erro vermelha abaixo do texto Now no IDE. Mas isso não é o suficiente. É legal quando o compilador informa que está faltando um ponto-e-vírgula ou uma chave em seu código. Obter informações sobre o erro é melhor do que nada, e quando o erro é simples fica fácil corrigi-lo com base na mensagem de erro. No entanto, não seria incrível se as ferramentas pudessem identificar os erros por conta própria? Eu gosto que me digam quando estou errado — e fico mais feliz ainda quando a mensagem de erro me fornece informações detalhadas explicando como o problema pode ser corrigido. E, se essas informações pudessem ser automatizadas, a ferramenta poderia resolver os problemas para mim. Seria menos tempo gasto por mim com esses problemas. Quanto mais economia de tempo, melhor.

É por isso que você pode ver no trecho de código anterior uma referência a uma classe denominada ChangeNowToUtcNowCodeAction. Essa classe implementa a interface ICodeAction, e o papel dela é mudar Now para UtcNow. O principal método que você deve implementar é chamado GetEdit. Nesse caso, o token Name no objeto MemberAccessExpressionSyntax precisa ser alterado para um novo token. Como mostra o código a seguir, é muito simples fazer essa substituição:

public CodeActionEdit GetEdit(CancellationToken cancellationToken)
{
  var nameNode = this.nowNode.Name;
  var utcNowNode =
    Syntax.IdentifierName("UtcNow");
  var rootNode = this.document.
    GetSyntaxRoot(cancellationToken);
  var newRootNode =
    rootNode.ReplaceNode(nameNode, utcNowNode);
  return new CodeActionEdit(
    document.UpdateSyntaxRoot(newRootNode));
}

Tudo o que você precisa fazer é criar um novo identificador com o texto UtcNow e substituir o token Now por esse novo identificador via ReplaceNode. Lembre-se de que as árvores de sintaxe são imutáveis, por isso você não pode mudar a árvore de documentos atual. Você deve criar uma nova árvore e retornar essa árvore chamando o método.

Com todo esse código pronto, você pode testá-lo no Visual Studio pressionando F5. Isso inicia uma nova instância do Visual Studio com a extensão automaticamente instalada.

Analisando construtores de DateTime

Esse é um bom começo, mas há mais casos a serem analisados. A classe DateTime possui vários construtores definidos que podem causar problemas. Há dois casos para os quais você deve dar especial atenção:

  1. O construtor poderá não usar o tipo de enumeração DateTimeKind como um de seus parâmetros, o que significa que o DateTime resultante estará em um estado não especificado (Unspecified).
  2. O construtor poderá usar o valor DateTimeKind como um de seus parâmetros, o que significa que você poderá especificar um valor de enumeração diferente de Utc.

Você pode escrever um código para localizar ambas as condições. No entanto, eu criarei apenas uma ação de código para o segundo cenário.

A Figura 3 lista o código para o método GetIssues na classe com base em ICodeIssue que localizará chamadas incorretas ao construtor DateTime.

Figura 3 Localizando chamadas incorretas ao construtor DateTime

public IEnumerable<CodeIssue> GetIssues(
  IDocument document, CommonSyntaxNode node, 
  CancellationToken cancellationToken)
{
  var creationNode = node as ObjectCreationExpressionSyntax;
  var creationNameNode = creationNode.Type as IdentifierNameSyntax;
  if (creationNameNode != null && 
    creationNameNode.Identifier.ValueText == "DateTime")
  {
    var model = document.GetSemanticModel();
    var creationSymbol = model.GetSymbolInfo(creationNode).Symbol;
    if (creationSymbol != null &&
      creationSymbol.ContainingType.ToDisplayString() ==
        Values.ExpectedContainingDateTimeTypeDisplayString &&
      creationSymbol.ContainingAssembly.ToDisplayString().Contains(
        Values.ExpectedContainingAssemblyDisplayString))
    {
      var argument = FindingNewDateTimeCodeIssueProvider
        .GetInvalidArgument(creationNode, model);
      if (argument != null)
      {
        if (argument.Item2.Name == "Local" ||
          argument.Item2.Name == "Unspecified")
        {
          return new [] { new CodeIssue(CodeIssueKind.Error,
            argument.Item1.Span,
            "Do not use DateTimeKind.Local or DateTimeKind.Unspecified",
            new ChangeDateTimeKindToUtcCodeAction(document, 
              argument.Item1)) };
        }
      }
      else
      {
        return new [] { new CodeIssue(CodeIssueKind.Error,
          creationNode.Span,
          "You must use a DateTime constuctor that takes a DateTimeKind") };
      }
    }
  }
  return null;
}

Isso é muito parecido com o outro problema. Uma vez entendido que o construtor vem de DateTime, é necessário avaliar os argumentos. (Eu explicarei o que GetInvalidArgument faz em alguns instantes.) Se você encontrar um argumento do tipo DateTimeKind e ele não especificar Utc, você tem um problema. Caso contrário, você sabe que está usando um construtor que não terá DateTime em Utc, então esse seria outro problema a ser relatado. A Figura 4 mostra a aparência de GetInvalidArgument.

Figura 4 O método GetInvalidArgument

private static Tuple<ArgumentSyntax, ISymbol> GetInvalidArgument(
  ObjectCreationExpressionSyntax creationToken, ISemanticModel model)
{
  foreach (var argument in creationToken.ArgumentList.Arguments)
  {
    if (argument.Expression is MemberAccessExpressionSyntax)
    {
      var argumentSymbolNode = model
        .GetSymbolInfo(argument.Expression).Symbol;
      if (argumentSymbolNode.ContainingType.ToDisplayString() ==
        Values.ExpectedContainingDateTimeKindTypeDisplayString)
      {
        return new Tuple<ArgumentSyntax,ISymbol>(argument, 
            argumentSymbolNode);
      }
    }
  }
  return null;
}

Esta pesquisa é muito parecida com as outras. Se o tipo de argumento for DateTimeKind, você sabe que tem um valor de argumento potencialmente inválido. Para corrigir o argumento, o código é quase idêntico à primeira ação de código que você viu aqui, por isso não o repetirei. Agora, se outros desenvolvedores tentarem contornar a restrição de DateTime.Now, você poderá pegá-los no pulo e corrigir também as chamadas do construtor!

No futuro

É maravilhoso pensar em todas as ferramentas que serão criadas com o Roslyn, mas ainda há muito trabalho a ser feito. Uma das maiores frustrações que acredito que você terá com o Roslyn nesse momento é a falta de documentação. Existem bons exemplos online e nos bits de instalação, mas o Roslyn é um conjunto de API amplo e pode ser complicado descobrir exatamente de onde começar e o que usar para executar uma tarefa específica. Não é incomum ter que pesquisar um pouco para descobrir as chamadas corretas a serem usadas. O aspecto estimulante é que eu geralmente consigo fazer algo no Roslyn que parecia bastante complexo, mas acaba levando menos de 100 ou 200 linhas de código.

Eu acredito que, quando o Roslyn estiver mais próximo do lançamento, tudo em torno dele será melhorado. E também estou convencido de que o Roslyn tem o potencial para dar suporte a várias estruturas e ferramentas no ecossistema do .NET. Eu não vejo muitos desenvolvedores do .NET usando as APIs do Roslyn todos os dias, mas você provavelmente passará a utilizar partes que usam o Roslyn em algum nível. É por isso que estou incentivando você a se aprofundar no Roslyn e descobrir como as coisas funcionam. Ser capaz de codificar expressões em regras reutilizáveis que qualquer desenvolvedor de uma equipe possa usar ajuda todos a produzir códigos melhores, rapidamente.

Jason Bock é líder de práticas na Magenic (magenic.com) e coautor do recente livro "Metaprogramming in .NET" (Manning Publications, 2013). Para entrar em contato com ele, escreva para jasonb@magenic.com.

AGRADECEMOS aos seguintes especialistas técnicos pela revisão deste artigo: Kevin Pilch-Bisson (Microsoft), Dustin Campbell, Jason Malinowski (Microsoft), Kirill Osenkov (Microsoft)