Introduzione all'analisi semantica
In questa esercitazione si presuppone una certa familiarità con l'API Syntax. L'articolo Introduzione all'analisi della sintassi offre informazioni introduttive sufficienti.
In questa esercitazione verranno esplorate le API Symbol e Binding. Queste API offrono informazioni sul significato semantico di un programma e consentono di porre domande sui tipi rappresentati da qualsiasi simbolo nel programma e ottenere risposte.
È necessario installare .NET Compiler Platform SDK:
Istruzioni di installazione: Programma di installazione di Visual Studio
Esistono due modi diversi per trovare .NET Compiler Platform SDK nel programma di installazione di Visual Studio:
Eseguire l'installazione con il Programma di installazione di Visual Studio: visualizzazione dei carichi di lavoro
.NET Compiler Platform SDK non viene selezionato automaticamente come parte del carico di lavoro Sviluppo di estensioni di Visual Studio. È necessario selezionarlo come componente facoltativo.
- Eseguire il programma di installazione di Visual Studio.
- Selezionare Modifica
- Selezionare il carico di lavoro Sviluppo di estensioni di Visual Studio.
- Aprire il nodo Sviluppo di estensioni di Visual Studio nell'albero di riepilogo.
- Selezionare la casella di controllo per .NET Compiler Platform SDK. È l'ultima voce dei componenti facoltativi.
Facoltativamente, è possibile installare anche l'editor DGML per visualizzare i grafici nel visualizzatore:
- Aprire il nodo Singoli componenti nell'albero di riepilogo.
- Selezionare la casella per l'editor DGML
Eseguire l'installazione usando il Programma di installazione di Visual Studio: scheda Singoli componenti
- Eseguire il programma di installazione di Visual Studio.
- Selezionare Modifica
- Selezionare la scheda Singoli componenti
- Selezionare la casella di controllo per .NET Compiler Platform SDK. È la prima voce nella sezione Compilatori, strumenti di compilazione e runtime.
Facoltativamente, è possibile installare anche l'editor DGML per visualizzare i grafici nel visualizzatore:
- Selezionare la casella di controllo per Editor DGML. La voce è disponibile nella sezione Strumenti per il codice.
Informazioni su compilazioni e simboli
Man mano che si lavora con .NET Compiler SDK si acquisirà familiarità con le distinzioni tra API Syntax e API Semantic. L'API Syntax consente di esaminare la struttura di un programma. Tuttavia, spesso servono informazioni più dettagliate sulla semantica o il significato di un programma. Anche se un file di codice libero o un frammento di codice Visual Basic o C# può essere analizzato in modo sintattico in isolamento, non è significativo porre domande come "qual è il tipo di questa variabile" in un vuoto. Il significato di un nome di tipo potrebbe essere dipendente da riferimenti ad assembly, importazioni di spazi dei nomi o altri file di codice. Queste domande possono ottenere risposta tramite l'API Semantic, in particolare la classe Microsoft.CodeAnalysis.Compilation.
Un'istanza di Compilation è paragonabile a un singolo progetto, dal punto di vista del compilatore e rappresenta tutti gli elementi necessari per compilare un programma Visual Basic o C#. La compilazione include il set di file di origine da compilare, i riferimenti agli assembly e le opzioni del compilatore. È possibile ragionare sul significato del codice usando tutte le altre informazioni in questo contesto. Compilation consente di trovare i simboli, ovvero entità come i tipi, gli spazi dei nomi, i membri e le variabili a cui fanno riferimento i nomi e altre espressioni. Il processo di associazione di nomi ed espressioni a simboli viene chiamato associazione.
Come Microsoft.CodeAnalysis.SyntaxTree, Compilation è una classe astratta con derivati specifici del linguaggio. Quando si crea un'istanza di Compilation, è necessario richiamare un metodo factory sulla classe Microsoft.CodeAnalysis.CSharp.CSharpCompilation (o Microsoft.CodeAnalysis.VisualBasic.VisualBasicCompilation).
Esecuzione di query sui simboli
In questa esercitazione, viene esaminato ancora una volta il programma "Hello World". In questo caso si esegue una query sui simboli nel programma per scoprire quali tipi rappresentano questi simboli. Verrà eseguita una query per recuperare i tipi in uno spazio dei nomi e si imparerà a trovare i metodi disponibili per un tipo.
È possibile visualizzare il codice completato per l'esempio nel repository GitHub.
Nota
I tipi di albero della sintassi usano l'ereditarietà per descrivere i diversi elementi della sintassi validi in posizioni diverse nel programma. L'uso di queste API spesso significa eseguire il cast di proprietà o membri di raccolte in tipi derivati specifici. Negli esempi seguenti, l'assegnazione e i cast sono istruzioni separate, con variabili tipizzate in modo esplicito. È possibile leggere il codice per visualizzare i tipi restituiti dell'API e il tipo di runtime degli oggetti restituiti. In pratica, è più comune usare variabili tipizzate in modo implicito e basarsi sui nomi delle API per descrivere il tipo di oggetti in corso di analisi.
Creare un nuovo progetto C# Stand-Alone Code Analysis Tool (Strumento di analisi del codice autonomo):
- In Visual Studio scegliere File>Nuovo>progetto per visualizzare la finestra di dialogo Nuovo progetto.
- In Visual C#>Estendibilità scegliere Strumento di analisi del codice autonomo.
- Denominare il progetto "SemanticQuickStart" e fare clic su OK.
Si analizzerà il programma "Hello World!" di base illustrato in precedenza.
Aggiungere il testo per il programma Hello World come costante nella 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!"");
}
}
}";
Aggiungere poi il codice seguente per creare l'albero della sintassi per il testo del codice nella costante programText
. Aggiungere la riga seguente al metodo Main
:
SyntaxTree tree = CSharpSyntaxTree.ParseText(programText);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();
Compilare poi un'istanza di CSharpCompilation dall'albero già creato. L'esempio "Hello World" si basa sui tipi String e Console. È necessario fare riferimento all'assembly che dichiara i due tipi nella compilazione. Aggiungere la riga seguente al metodo Main
per creare una compilazione dell'albero della sintassi, incluso il riferimento all'assembly appropriato:
var compilation = CSharpCompilation.Create("HelloWorld")
.AddReferences(MetadataReference.CreateFromFile(
typeof(string).Assembly.Location))
.AddSyntaxTrees(tree);
Il metodo CSharpCompilation.AddReferences aggiunge i riferimenti alla compilazione. Il metodo MetadataReference.CreateFromFile carica un assembly come riferimento.
Esecuzione di query sul modello semantico
Dopo aver creato un Compilation è possibile richiedere un SemanticModel per qualsiasi SyntaxTree contenuto in tale Compilation. È possibile considerare il modello semantico come fonte di tutte le informazioni che si ottengono in genere da IntelliSense. Un SemanticModel può rispondere a domande come "Quali nomi sono inclusi nell'ambito in questa posizione?", "Quali membri sono accessibili da questo metodo?", "Quali variabili vengono usate in questo blocco di testo?" e "A cosa fa riferimento questo nome/espressione?" Aggiungere questa istruzione per creare il modello semantico:
SemanticModel model = compilation.GetSemanticModel(tree);
Associazione di un nome
Crea Compilation l'oggetto SemanticModel dall'oggetto SyntaxTree. Dopo aver creato il modello, è possibile eseguire query per trovare la prima direttiva using
e recuperare informazioni sui simboli per lo spazio dei nomi System
. Aggiungere queste due righe al metodo Main
per creare il modello semantico e recuperare il simbolo per la prima istruzione 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);
Il codice precedente illustra come associare il nome nella prima direttiva using
per recuperare un Microsoft.CodeAnalysis.SymbolInfo per lo spazio dei nomi System
. Il codice precedente dimostra anche che si usa il modello della sintassi per trovare la struttura del codice e il modello semantico per comprenderne il significato. Il modello della sintassi individua la stringa System
nell'istruzione using. Il modello semantico include tutte le informazioni sui tipi definiti nello spazio dei nomi System
.
Dall'oggetto SymbolInfo è possibile ottenere Microsoft.CodeAnalysis.ISymbol usando la proprietà SymbolInfo.Symbol. Questa proprietà restituisce il simbolo a cui fa riferimento questa espressione. Per le espressioni che non fanno riferimento ad alcun elemento (ad esempio, i valori letterali numerici) questa proprietà è null
. Quando SymbolInfo.Symbol non è null, ISymbol.Kind indica il tipo del simbolo. In questo esempio, la proprietà ISymbol.Kind è un SymbolKind.Namespace. Aggiungere il codice seguente al metodo Main
. Recupera il simbolo per lo spazio dei nomi System
e quindi visualizza tutti gli spazi dei nomi figlio dichiarati nello spazio dei nomi System
:
var systemSymbol = (INamespaceSymbol?)nameInfo.Symbol;
if (systemSymbol?.GetNamespaceMembers() is not null)
{
foreach (INamespaceSymbol ns in systemSymbol?.GetNamespaceMembers()!)
{
Console.WriteLine(ns);
}
}
Eseguire il programma. L'output dovrebbe essere il seguente:
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 . . .
Nota
L'output non include ogni spazio dei nomi figlio dello spazio dei nomi System
. Viene visualizzato ogni spazio dei nomi presente in questa compilazione, che fa riferimento solo all'assembly in cui è dichiarato System.String
. Gli eventuali spazi dei nomi dichiarati in altri assembly non sono noti per questa compilazione
Associazione di un'espressione
Il codice precedente mostra come trovare un simbolo mediante l'associazione a un nome. Esistono altre espressioni in un programma C# che possono essere associate e non sono nomi. Per illustrare questa funzionalità, si esaminerà l'associazione a un semplice valore letterale stringa.
Il programma "Hello World" contiene una Microsoft.CodeAnalysis.CSharp.Syntax.LiteralExpressionSyntaxstringa , "Hello, World!" visualizzata nella console.
Trova la stringa "Hello, World!" individuando il valore letterale stringa singolo nel programma. Dopo aver individuato il nodo della sintassi, è possibile ottenere le informazioni sul tipo per tale nodo dal modello semantico. Aggiungere il codice seguente al metodo 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);
Lo struct Microsoft.CodeAnalysis.TypeInfo include una proprietà TypeInfo.Type che consente l'accesso alle informazioni semantiche sul tipo del valore letterale. In questo esempio, si tratta del tipo string
. Aggiungere una dichiarazione che assegna questa proprietà a una variabile locale:
var stringTypeSymbol = (INamedTypeSymbol?)literalInfo.Type;
Per completare questa esercitazione, verrà definita una query LINQ che crea una sequenza di tutti i metodi pubblici dichiarati nel tipo string
che restituiscono string
. Questa query diventa complessa, quindi viene creata riga per riga per poi essere ricostruita come singola query. L'origine per questa query è la sequenza di tutti i membri dichiarati nel tipo string
:
var allMembers = stringTypeSymbol?.GetMembers();
Questa sequenza di origine contiene tutti i membri, inclusi le proprietà e i campi, quindi filtrarla tramite il metodo ImmutableArray<T>.OfType per trovare gli elementi corrispondenti a oggetti Microsoft.CodeAnalysis.IMethodSymbol:
var methods = allMembers?.OfType<IMethodSymbol>();
Aggiungere poi un altro filtro per restituire solo i metodi pubblici che restituiscono string
:
var publicStringReturningMethods = methods?
.Where(m => SymbolEqualityComparer.Default.Equals(m.ReturnType, stringTypeSymbol) &&
m.DeclaredAccessibility == Accessibility.Public);
Selezionare solo la proprietà del nome e solo i nomi distinti rimuovendo qualsiasi overload:
var distinctMethods = publicStringReturningMethods?.Select(m => m.Name).Distinct();
È anche possibile compilare la query completa usando la sintassi della query LINQ e quindi visualizzare tutti i nomi dei metodi nella 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);
}
Compilare ed eseguire il programma. Verrà visualizzato l'output seguente:
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 . . .
È stata usata l'API Semantic per trovare e visualizzare informazioni sui simboli che fanno parte di questo programma.