Introdução à análise semântica

Este tutorial presume que você está familiarizado com a API de sintaxe. O artigo Introdução à a análise de sintaxe fornece uma introdução suficiente.

Neste tutorial, você explora as APIs de Símbolo e de Associação. Essas APIs fornecem informações sobre o significado semântico de um programa. Elas permitem fazer e responder perguntas sobre os tipos representado por qualquer símbolo em seu programa.

Você deverá instalar o SDK do .NET Compiler Platform:

Instruções de instalação – Instalador do Visual Studio

Há duas maneiras diferentes de encontrar o SDK da .NET Compiler Platform no Instalador do Visual Studio:

Instalar usando o Instalador do Visual Studio – exibição de cargas de trabalho

O SDK da .NET Compiler Platform não é selecionado automaticamente como parte da carga de trabalho de desenvolvimento da extensão do Visual Studio. É necessário selecioná-lo como um componente opcional.

  1. Execute o Instalador do Visual Studio
  2. Escolha Mudar
  3. Marque a carga de trabalho de Desenvolvimento de extensão do Visual Studio.
  4. Abra o nó Desenvolvimento de extensão do Visual Studio na árvore de resumo.
  5. Marque a caixa do SDK da .NET Compiler Platform. Você a encontrará por último nos componentes opcionais.

Opcionalmente, você também poderá fazer o Editor DGML exibir gráficos no visualizador:

  1. Abra o nó Componentes individuais na árvore de resumo.
  2. Marque a caixa do Editor DGML

Instalar usando o Instalador do Visual Studio – guia Componentes individuais

  1. Execute o Instalador do Visual Studio
  2. Escolha Mudar
  3. Selecione a guia Componentes individuais
  4. Marque a caixa do SDK da .NET Compiler Platform. Você a encontrará na parte superior, na seção Compiladores, ferramentas de compilação e runtimes.

Opcionalmente, você também poderá fazer o Editor DGML exibir gráficos no visualizador:

  1. Marque a caixa do Editor DGML. Você a encontrará na seção Ferramentas de código.

Noções básicas sobre compilações e símbolos

Conforme você trabalha mais com o SDK do .NET Compiler, você se familiariza com as distinções entre a API de Sintaxe e a API de Semântica. A API de Sintaxe permite que você examine a estrutura de um programa. Muitas vezes, no entanto, você deseja as informações sobre a semântica ou significado de um programa. Embora um snippet ou arquivo de código livre do Visual Basic ou C# possa ser analisado sintaticamente de modo isolado, não faz sentido fazer a esmo perguntas como "qual é o tipo dessa variável". O significado de um nome de tipo pode ser dependente de referências de assembly, importações de namespace ou outros arquivos de código. Essas perguntas são respondidas usando-se a API de Semântica, especificamente a classe Microsoft.CodeAnalysis.Compilation.

Uma instância de Compilation é análoga a um único projeto conforme visto pelo compilador e representa tudo o que é necessário para compilar um programa Visual Basic ou C#. A compilação inclui o conjunto de arquivos de origem a serem compilados, referências de assembly e opções de compilador. Você pode avaliar o significado do código usando todas as outras informações neste contexto. Um Compilation permite que você encontre símbolos – entidades como tipos, namespaces, membros e variáveis aos quais os nomes e outras expressões se referem. O processo de associar nomes e expressões com símbolos é chamado de associação.

Assim como Microsoft.CodeAnalysis.SyntaxTree, Compilation é uma classe abstrata com derivativos específicos a um idioma. Ao criar uma instância de compilação, você deve invocar um método de fábrica na classe Microsoft.CodeAnalysis.CSharp.CSharpCompilation (ou Microsoft.CodeAnalysis.VisualBasic.VisualBasicCompilation).

Consultar símbolos

Neste tutorial, você analisa novamente o programa "Olá, Mundo". Dessa vez, você consulta os símbolos no programa para compreender quais tipos esses símbolos representam. Você consulta os tipos em um namespace e aprende a localizar os métodos disponíveis em um tipo.

Você pode ver o código concluído para essa amostra no nosso repositório do GitHub.

Observação

Os tipos de árvore de sintaxe usam a herança para descrever os elementos de sintaxe diferentes que são válidos em locais diferentes no programa. Usar essas APIs geralmente significa converter propriedades ou membros da coleção em tipos derivados específicos. Nos exemplos a seguir, a atribuição e as conversões são instruções separadas, usando variáveis explicitamente tipadas. Você pode ler o código para ver os tipos de retorno da API e o tipo de runtime dos objetos retornados. Na prática, é mais comum usar variáveis implicitamente tipadas e depender de nomes de API para descrever o tipo de objeto que está sendo examinado.

Criar um novo projeto de Ferramenta de Análise de Código Autônoma do C#:

  • No Visual Studio, escolha Arquivo>Novo>Projeto para exibir a caixa de diálogo Novo Projeto.
  • Em Visual C#>Extensibilidade, escolha Ferramenta de Análise de Código Autônoma.
  • Nomeie o projeto "SemanticQuickStart" e clique em OK.

Você vai analisar o programa básico "Olá, Mundo!" mostrado anteriormente. Adicione o texto ao programa Olá, Mundo como uma constante em sua classe Program:

        const string programText =
@"using System;
using System.Collections.Generic;
using System.Text;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(""Hello, World!"");
        }
    }
}";

Em seguida, adicione o código a seguir para criar a árvore de sintaxe para o texto do código na constante programText. Adicione a seguinte linha ao seu método Main:

SyntaxTree tree = CSharpSyntaxTree.ParseText(programText);

CompilationUnitSyntax root = tree.GetCompilationUnitRoot();

Em seguida, compile uma CSharpCompilation da árvore que você já criou. A amostra "Olá, Mundo" depende dos tipos String e Console. Você precisa fazer referência ao assembly que declara esses dois tipos em sua compilação. Adicione a seguinte linha ao seu método Main para criar uma compilação de sua árvore de sintaxe, incluindo a referência ao assembly apropriado:

var compilation = CSharpCompilation.Create("HelloWorld")
    .AddReferences(MetadataReference.CreateFromFile(
        typeof(string).Assembly.Location))
    .AddSyntaxTrees(tree);

O método CSharpCompilation.AddReferences adiciona referências à compilação. O método MetadataReference.CreateFromFile carrega um assembly como uma referência.

Consultar o modelo semântico

Assim que você tiver uma Compilation, você poderá solicitar a ela um SemanticModel para qualquer SyntaxTree contida nessa Compilation. Você pode pensar no modelo semântico como a origem de todas as informações normalmente obtidas do IntelliSense. Um SemanticModel pode responder a perguntas como "O que são nomes no escopo nesse local?", "Quais membros são acessíveis deste método?", "Quais variáveis são usadas neste bloco de texto?" e "A que este nome/expressão se refere?" Adicione esta instrução para criar o modelo semântico:

SemanticModel model = compilation.GetSemanticModel(tree);

Associar um nome

A Compilation cria o SemanticModel da SyntaxTree. Depois de criar o modelo, você pode consultar para localizar a primeira diretiva using e recuperar as informações de símbolo para o namespace System. Adicione estas duas linhas a seu método Main para criar o modelo semântico e recuperar o símbolo para a primeira instrução using:

// Use the syntax tree to find "using System;"
UsingDirectiveSyntax usingSystem = root.Usings[0];
NameSyntax systemName = usingSystem.Name;

// Use the semantic model for symbol information:
SymbolInfo nameInfo = model.GetSymbolInfo(systemName);

O código anterior mostra como associar o nome na primeira diretiva using para recuperar um Microsoft.CodeAnalysis.SymbolInfo para o namespace System. O código anterior também ilustra o uso da sintaxe de modelo para localizar a estrutura do código; você usa o modelo semântico para entender seu significado. A sintaxe de modelo localiza a cadeia de caracteres System na instrução using. O modelo semântico tem todas as informações sobre os tipos definidos no namespace System.

Do objeto SymbolInfo, você pode obter o Microsoft.CodeAnalysis.ISymbol usando a propriedade SymbolInfo.Symbol. Essa propriedade retorna o símbolo a que essa expressão se refere. Para expressões que não se referem a nada (como literais numéricos), essa propriedade é null. Quando o SymbolInfo.Symbol não for null, o ISymbol.Kind denotará o tipo do símbolo. Nesse exemplo, a propriedade ISymbol.Kind é um SymbolKind.Namespace. Adicione o código a seguir ao método Main. Ele recupera o símbolo para o namespace System e, em seguida, exibe todos os namespaces filho declarados no namespace System:

var systemSymbol = (INamespaceSymbol?)nameInfo.Symbol;
if (systemSymbol?.GetNamespaceMembers() is not null)
{
    foreach (INamespaceSymbol ns in systemSymbol?.GetNamespaceMembers()!)
    {
        Console.WriteLine(ns);
    }
}

Execute o programa e você deverá ver a seguinte saída:

System.Collections
System.Configuration
System.Deployment
System.Diagnostics
System.Globalization
System.IO
System.Numerics
System.Reflection
System.Resources
System.Runtime
System.Security
System.StubHelpers
System.Text
System.Threading
Press any key to continue . . .

Observação

A saída não inclui todos os namespaces que são namespaces filhos do namespace System. El exibe cada namespace presente nessa compilação, que só referencia o assembly em que System.String é declarada. Quaisquer outros namespaces declarados em outros assemblies não são conhecidos desta compilação

Associar uma expressão

O código anterior mostra como encontrar um símbolo associando-o a um nome. Há outras expressões em um programa C# que podem ser associadas que não são nomes. Para demonstrar essa capacidade, acessaremos a associação a um único literal de cadeia de caracteres.

O programa "Olá, Mundo" contém Microsoft.CodeAnalysis.CSharp.Syntax.LiteralExpressionSyntax, uma cadeia de caracteres "Olá, Mundo!" exibida no console.

Você encontra a cadeia de caracteres "Olá, Mundo!" localizando o único literal de cadeia de caracteres no programa. Em seguida, depois de localizar o nó de sintaxe, você obtém as informações de tipo para esse nó do modelo semântico. Adicione o código a seguir ao método Main:

// Use the syntax model to find the literal string:
LiteralExpressionSyntax helloWorldString = root.DescendantNodes()
.OfType<LiteralExpressionSyntax>()
.Single();

// Use the semantic model for type information:
TypeInfo literalInfo = model.GetTypeInfo(helloWorldString);

O struct Microsoft.CodeAnalysis.TypeInfo inclui uma propriedade TypeInfo.Type que permite o acesso às informações semânticas sobre o tipo do literal. Neste exemplo, ele é do tipo string. Adicione uma declaração que atribui essa propriedade a uma variável local:

var stringTypeSymbol = (INamedTypeSymbol?)literalInfo.Type;

Para concluir este tutorial, criaremos uma consulta LINQ que criará uma sequência de todos os métodos públicos declarados no tipo string que retorna um string. Essa consulta torna-se complexa, então a compilaremos linha a linha e então a reconstruiremos como uma única consulta. A ordem desta consulta é a sequência de todos os membros declarados no tipo string:

var allMembers = stringTypeSymbol?.GetMembers();

Essa sequência de origem contém todos os membros, incluindo propriedades e campos, portanto, filtre-a usando o método ImmutableArray<T>.OfType para localizar elementos que são objetos Microsoft.CodeAnalysis.IMethodSymbol:

var methods = allMembers?.OfType<IMethodSymbol>();

Em seguida, adicione outro filtro para retornar somente os métodos que são públicos e retornam um string:

var publicStringReturningMethods = methods?
    .Where(m => SymbolEqualityComparer.Default.Equals(m.ReturnType, stringTypeSymbol) &&
    m.DeclaredAccessibility == Accessibility.Public);

Selecione apenas a propriedade de nome e somente os nomes distintos, removendo quaisquer sobrecargas:

var distinctMethods = publicStringReturningMethods?.Select(m => m.Name).Distinct();

Você pode também compilar a consulta completa usando a sintaxe de consulta LINQ e, em seguida, exibir todos os nomes de método no console:

foreach (string name in (from method in stringTypeSymbol?
                         .GetMembers().OfType<IMethodSymbol>()
                         where SymbolEqualityComparer.Default.Equals(method.ReturnType, stringTypeSymbol) &&
                         method.DeclaredAccessibility == Accessibility.Public
                         select method.Name).Distinct())
{
    Console.WriteLine(name);
}

Compile e execute o programa. Você deve ver o seguinte resultado:

Join
Substring
Trim
TrimStart
TrimEnd
Normalize
PadLeft
PadRight
ToLower
ToLowerInvariant
ToUpper
ToUpperInvariant
ToString
Insert
Replace
Remove
Format
Copy
Concat
Intern
IsInterned
Press any key to continue . . .

Você usou a API de semântica para localizar e exibir informações sobre os símbolos que fazem parte deste programa.