Featured image of post Lições aprendidas ao construir um analisador de código estático para C#

Lições aprendidas ao construir um analisador de código estático para C#

Eu construí um analisador de código estático Roslyn para permitir o uso de uniões distribuídas

Introdução Link to this section

Analisadores de código estático são ferramentas usadas para analisar o código de software sem executá-lo. Eles podem examinar o código para encontrar code smells, vulnerabilidades, erros potenciais e código fora de um padrão definido, por exemplo. Eles funcionam analisando o código-fonte e avaliando sua sintaxe (estrutura do código) e semântica (significado do código).

Roslyn, o compilador C#, fornece ferramentas para desenvolver Roslyn Analyzers (Analisadores de código estático para Roslyn), dando acesso à sintaxe e semântica do código, que podem ser executados em tempo de desenvolvimento e build, fornecendo feedback quase em tempo real para os desenvolvedores.

Neste post, mostrarei um Analisador Roslyn que construí para usar Uniões Discriminadas em C#, exigindo verificar o tipo de união antes do acesso, e falarei sobre algumas lições aprendidas ao desenvolvê-lo.

Pacote DiscriminatedUnions.Net Link to this section

O pacote permite o uso de Uniões Discriminadas, aplicando duas regras:

  1. O tipo de uma União deve ser verificado antes do acesso;
  2. Todos os tipos de uma União devem ser verificados (ou ter um caso else/default/discard).

Para usar o pacote, instale-o no projeto:

<PackageReference Include="DiscriminatedUnions.Net" Version="1.0.0.19" />

Estenda UnionValue ao declarar os tipos que serão usados na União Discriminada:

1
2
3
4
5
6
7
public class Bird: UnionValue
{
}

public class Dog : UnionValue
{
}

Declare o membro Union passando os tipos de valor possíveis:

Union<Dog, Bird> animal = new Dog();

E acesse-o verificando o tipo com If, Switch/Case ou Pattern Matching:

1
2
3
4
5
6
if (animal.Value is Dog)
{
    Console.WriteLine($"Dog: {animal.Value}");
}
else
...

Acessar o objeto sem verificar gerará um erro DUN002 - 'animal.Value' not checked before access:

Não verificar todos os tipos possíveis gerará um erro DUN001 - 'animal' not being evaluated for all possible types:

⚠️ Este pacote foi feito apenas para fins de estudo. Sinta-se à vontade para testá-lo, mas não o use para código de produção, pois ele não será mantido e pode não cobrir todos os casos extremos.

Código fonte do DiscriminatedUnions.NET Link to this section

https://github.com/dgenezini/DiscriminatedUnions.NET

Lições aprendidas Link to this section

Pacotes NuGet somente para tempo de desenvolvimento Link to this section

Os analisadores Roslyn podem ser distribuídos como pacotes NuGet, mas é importante gerar o pacote como uma Dependência de Desenvolvimento apenas, definindo a propriedade DevelopmentDependency como true no arquivo csproj:

<DevelopmentDependency>true</DevelopmentDependency>

Isso gerará duas tags nas propriedades do pacote NuGet dos projetos consumidores:

1
2
3
4
<PackageReference Include="DiscriminatedUnions.Net.Analyzers" Version="1.0.0.1">
    <PrivateAssets>all</PrivateAssets>
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
  • PrivateAssets com o valor all, indicando que este pacote não fluirá para projetos dependendo do projeto consumidor. Por exemplo, em um cenário onde Project1 depende do pacote DiscriminatedUnions.Net.Analyzers e Project2 depende de Project1, o analisador não estará disponível para Project2;
  • IncludeAssets sem o valor compile em seu valor, indicando que o consumidor do pacote não terá acesso às suas assemblies compiladas.

Os analisadores Roslyn podem ser usados para aplicar regras sobre como usar um pacote Link to this section

Um ótimo caso de uso para Roslyn Analyzers é aplicar regras em pacotes NuGet.

O pacote DistributedUnion.Net é um exemplo. Ele tem os tipos Union e UnionValue e analisadores para aplicar as regras em seu uso.

Como as classes dentro dos pacotes Analyzers não são acessíveis pelos consumidores (e não deveriam ser), a maneira mais correta, na minha opinião, é ter as classes públicas em um pacote e este pacote consumirá outro pacote com os analisadores, mas removendo a configuração PrivateAssets. Desta forma, os analisadores estarão disponíveis para os consumidores do pacote pai:

1
2
3
<PackageReference Include="DiscriminatedUnions.Net.Analyzers" Version="0.1.0.0-beta">
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

Os analisadores têm que evoluir junto com a linguagem Link to this section

Como os analisadores usam a sintaxe do código, à medida que novas maneiras de fazer algo são adicionadas à linguagem, os analisadores têm que ser atualizados para tratá-los também. Por exemplo:

  • Um analisador verificando as condições antes da correspondência de padrões ser lançado, precisaria ser atualizado para considerar esta nova sintaxe;
  • Um analisador usando tipos para suas regras, precisaria ser atualizado para considerar tipos de referência anuláveis em sua lógica.

TDD é seu amigo Link to this section

O modelo de projeto Roslyn Analyzers vem com um projeto VSIX que pode ser executado para testar os analisadores, mas não consegui fazê-los funcionar e decidi que não valia a pena o tempo.

Acontece que a melhor e mais fácil maneira de executar e experimentar o analisador é executando testes automatizados. Crie o teste, execute o teste e altere o analisador até que o teste passe.

Exemplo de um teste automatizado que espera o código de erro DUN001 no local 0 com o argumento unionBird:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[TestMethod]
        public async Task Notify_If_NotCheckingAllCases_Many()
        {
            var test = @"
using DiscriminatedUnionsNet;

class Program
{
    static string GetValue()
    {
        Union<Duck, Goose, Eagle, Crow> unionBird = new Duck();

        if ({|#0:unionBird|}.Value is Goose)
        {
            return ""Goose"";
        }

        return null;
    }
}" + defineUnion;

            var expected = VerifyCS
                .Diagnostic("DUN001")
                .WithLocation(0)
                .WithArguments("unionBird");

            await VerifyCS.VerifyAnalyzerAsync(test, expected);
        }

O local e o argumento são marcados com a sintaxe {|#location:argument|}.

ℹ️ O modelo de projeto vem com alguns testes básicos como exemplos que são fáceis de alterar.

Casts… Casts em todos os lugares Link to this section

Os métodos e propriedades dos analisadores definem tipos de interface gerais que precisam ser convertidos para o tipo específico antes de acessar suas propriedades.

Este é um trecho do pacote DiscriminatedUnions.Net:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var namedType = (INamedTypeSymbol)typeInfo.Type; //typeInfo.Type is ISymbol

if ((!namedType.Name.Equals("UnionValue")) ||
    (!namedType.ContainingNamespace.Name.Equals("DiscriminatedUnionsNet")))
{
    return;
}

...

if (parent is ISwitchOperation switchOperation) //parent is IOperation
{
    if (!(switchOperation.Value is IPropertyReferenceOperation propertyReferenceOperation) || //switchOperation.Value is IOperation
        !(propertyReferenceOperation.Instance is ILocalReferenceOperation parentReferenceOperation))
    {
        parent = parent.Parent;

        continue;
    }
    ...
}
else if (parent is ISwitchExpressionOperation switchExpressionOperation) //parent is IOperation
{
    ...
}

Entendendo a árvore de sintaxe e encontrando as interfaces corretas Link to this section

A maneira mais fácil de entender a árvore de sintaxe é usando o Syntax Visualizer do Visual Studio (Instruções de instalação).

Basta clicar em qualquer lugar no código e ele mostrará a árvore de sintaxe até aquele ponto:

Outra dica é digitar ISymbol, IOperation ou qualquer interface base para ver todas as interfaces específicas no intellisense:

Pouco material para ajudar os iniciantes Link to this section

Como os analisadores Roslyn têm casos de uso muito específicos, não há muito material e documentação online. Aqui estão alguns links que me ajudaram a aprender (especialmente o Blog de Josh Varty e Meziantou):

Blog de Josh Varty - Série Learn Roslyn Now

Blog de Meziantou - Série Writing a Roslyn analyzer

Documentos Roslyn - Como começar

Documentos Roslyn GitHub - Como escrever um analisador C# e correção de código

Documentos Roslyn GitHub - Semântica de ações do analisador

💬 Like or have something to add? Leave a comment below.
Ko-fi
GitHub Sponsor
Licensed under CC BY-NC-SA 4.0
Criado com Hugo
Tema Stack desenvolvido por Jimmy