Bagikan melalui


Memulai analisis sintaks

Dalam tutorial ini, Anda akan menjelajahi Syntax API. Syntax API menyediakan akses ke struktur data yang menjelaskan program C# atau Visual Basic. Struktur data ini memiliki detail yang cukup sehingga mereka dapat sepenuhnya mewakili program apa pun dengan ukuran berapa pun. Struktur ini dapat menjelaskan program lengkap yang dikompilasi dan dijalankan dengan benar. Mereka juga dapat menjelaskan program yang tidak lengkap, saat Anda menulisnya, di penyunting.

Untuk mengaktifkan ekspresi kaya ini, struktur data dan API yang membentuk Syntax API harus kompleks. Mari kita mulai dengan seperti apa struktur data untuk program "Halo Dunia" pada umumnya:

using System;
using System.Collections.Generic;
using System.Linq;

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

Lihat teks program sebelumnya. Anda mengenali elemen yang sudah dikenal. Seluruh teks mewakili satu file sumber, atau unit kompilasi. Tiga baris pertama dari file sumber tersebut adalah menggunakan direktif. Sumber yang tersisa terkandung dalam deklarasi namespace layanan . Deklarasi namespace layanan berisi deklarasi kelas turunan. Deklarasi kelas berisi satu deklarasi metode.

Syntax API membuat struktur pohon dengan akar yang mewakili unit kompilasi. Node di pohon mewakili direktif penggunaan, deklarasi namespace layanan, dan semua elemen program lainnya. Struktur pohon berlanjut ke tingkat terendah: string "Halo Dunia!" adalah token harfiah string yang merupakan turunan dari argumen. Syntax API menyediakan akses ke struktur program. Anda dapat mengkueri praktik kode tertentu, menelusuri seluruh pohon untuk memahami kode, dan membuat pohon baru dengan memodifikasi pohon yang ada.

Deskripsi singkat tersebut memberikan gambaran umum tentang jenis informasi yang dapat diakses menggunakan Syntax API. Syntax API tidak lebih dari API formal yang menjelaskan konstruksi kode yang sudah dikenal yang Anda ketahui dari C#. Kemampuan penuh mencakup informasi tentang bagaimana kode diformat termasuk pembatas baris, spasi, dan inden. Dengan menggunakan informasi ini, Anda dapat sepenuhnya mewakili kode seperti yang ditulis dan dibaca oleh pemrogram manusia atau pengompilasi. Menggunakan struktur ini memungkinkan Anda untuk berinteraksi dengan kode sumber pada tingkat yang sangat bermakna. Struktur ini bukan lagi string teks, tetapi data yang mewakili struktur program C#.

Untuk memulai, Anda harus menginstal .NET Compiler Platform SDK:

Petunjuk penginstalan - Alat Penginstal Visual Studio

Ada dua cara berbeda untuk menemukan .NET Compiler Platform SDK di Alat penginstal Visual Studio:

Instal menggunakan Alat penginstal Visual Studio - Tampilan beban kerja

.NET Compiler Platform SDK tidak secara otomatis dipilih sebagai bagian dari beban kerja pengembangan ekstensi Visual Studio. Anda harus memilihnya sebagai komponen opsional.

  1. Jalankan Alat penginstal Visual Studio
  2. Pilih Ubah
  3. Periksa beban kerja pengembangan ekstensi Visual Studio.
  4. Buka simpul Pengembangan ekstensi Visual Studio di pohon ringkasan.
  5. Centang kotak untuk .NET Compiler Platform SDK. Anda akan menemukannya terakhir di bawah komponen opsional.

Secara opsional, Anda juga ingin penyunting DGML menampilkan grafik di visualizer:

  1. Buka simpul Komponen individu di pohon ringkasan.
  2. Centang kotak untuk Penyunting DGML

Instal menggunakan Alat penginstal Visual Studio - tab komponen individu

  1. Jalankan Alat penginstal Visual Studio
  2. Pilih Ubah
  3. Pilih tab Komponen individu
  4. Centang kotak untuk .NET Compiler Platform SDK. Anda akan menemukannya di bagian atas di bawah bagian Pengompilasi, alat build, dan runtime.

Secara opsional, Anda juga ingin penyunting DGML menampilkan grafik di visualizer:

  1. Centang kotak untuk Penyunting DGML. Anda akan menemukannya di bagian Alat kode.

Memahami pohon sintaksis

Anda menggunakan Syntax API untuk setiap analisis struktur kode C#. API Sintaks mengekspos pengurai, pohon sintaks, dan utilitas untuk menganalisis dan membangun pohon sintaksis. Ini adalah cara Anda mencari kode untuk elemen sintaks tertentu atau membaca kode untuk program.

Pohon sintaks adalah struktur data yang digunakan oleh pengompilasi C# dan Visual Basic untuk memahami program C# dan Visual Basic. Pohon sintaks diproduksi oleh parser yang sama yang berjalan ketika sebuah proyek dibangun atau pengembang mencapai F5. Pohon sintaks memiliki kesetiaan penuh dengan bahasa; setiap bit informasi dalam file kode diwakili di pohon. Menulis pohon sintaks ke teks mereproduksi teks asli yang sama persis yang telah diuraikan. Pohon sintaksis juga tidak dapat diubah ; setelah dibuat pohon sintaks tidak akan pernah bisa diubah. Konsumen pohon dapat menganalisis pohon pada beberapa utas, tanpa kunci atau langkah-langkah konkurensi lainnya, mengetahui data tidak pernah berubah. Anda dapat menggunakan API untuk membuat pohon baru yang merupakan hasil dari modifikasi pohon yang sudah ada.

Empat blok penyusun utama dari pohon sintaks adalah:

Trivia, token, dan node disusun secara hierarkis untuk membentuk pohon yang sepenuhnya mewakili segala sesuatu dalam fragmen kode Visual Basic atau C#. Anda dapat melihat struktur ini menggunakan jendela Syntax Visualizer. Di Visual Studio, pilih Lihat>Windows Lainnya>Syntax Visualizer. Misalnya, file sumber C# sebelumnya yang diperiksa menggunakan Syntax Visualizer terlihat seperti gambar berikut:

SintaksNode: Biru | SyntaxToken: Hijau | SintaksTrivia: File Kode C# Merah

Dengan menavigasi struktur pohon ini, Anda dapat menemukan pernyataan, ekspresi, token, atau sedikit ruang kosong apa pun dalam file kode.

Meskipun Anda dapat menemukan apa pun dalam file kode menggunakan Syntax API, sebagian besar skenario melibatkan pemeriksaan cuplikan kecil kode, atau mencari pernyataan atau fragmen tertentu. Dua contoh berikut menunjukkan kegunaan khas untuk menelusuri struktur kode, atau mencari pernyataan tunggal.

Melintasi pohon

Anda dapat memeriksa node dalam pohon sintaks dengan dua cara. Anda dapat melintasi pohon untuk memeriksa setiap node, atau Anda dapat mengkueri untuk elemen atau node tertentu.

Lintasan manual

Anda dapat melihat kode yang telah selesai untuk contoh ini di repositori GitHub kami.

Catatan

Jenis Pohon Sintaks menggunakan pewarisan untuk menggambarkan elemen sintaks berbeda yang valid di lokasi berbeda dalam program. Menggunakan API ini sering kali berarti mentransmisikan properti atau anggota koleksi ke jenis turunan tertentu. Dalam contoh berikut, penugasan dan pemeran adalah pernyataan terpisah, menggunakan variabel yang diketik secara eksplisit. Anda dapat membaca kode untuk melihat jenis kembalian API dan jenis runtime bahasa umum dari objek yang dikembalikan. Dalam praktiknya, lebih umum menggunakan variabel yang diketik secara implisit dan mengandalkan nama API untuk mendeskripsikan jenis objek yang diperiksa.

Buat proyek C# Stand-Alone Code Analysis Tool baru:

  • Di Visual Studio, pilih File>Baru>Proyek untuk menampilkan dialog Proyek Baru.
  • Di bawah Visual C#>Extensibility, pilih Stand-Alone Code Analysis Tool.
  • Beri nama proyek Anda " SyntaxTreeManualTraversal " dan klik OK.

Anda akan menganalisis dasar "Halo Dunia!" program yang ditampilkan sebelumnya. Tambahkan teks untuk program Halo Dunia sebagai konstanta di kelas Program Anda:

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

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

Selanjutnya, tambahkan kode berikut untuk membangun pohon sintaksis untuk teks kode dalam konstanta programText. Tambahkan baris berikut ke metode Main Anda:

SyntaxTree tree = CSharpSyntaxTree.ParseText(programText);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();

Kedua garis tersebut membuat pohon dan mengambil node akar dari pohon itu. Anda sekarang dapat memeriksa node di pohon. Tambahkan baris ini ke metode Main Anda untuk menampilkan beberapa properti node akar di pohon:

WriteLine($"The tree is a {root.Kind()} node.");
WriteLine($"The tree has {root.Members.Count} elements in it.");
WriteLine($"The tree has {root.Usings.Count} using statements. They are:");
foreach (UsingDirectiveSyntax element in root.Usings)
    WriteLine($"\t{element.Name}");

Jalankan aplikasi untuk melihat apa yang ditemukan kode Anda tentang node akar di pohon ini.

Biasanya, Anda akan melintasi pohon untuk mempelajari kodenya. Dalam contoh ini, Anda menganalisis kode yang Anda ketahui untuk menjelajahi API. Tambahkan kode berikut untuk memeriksa anggota pertama dari node root:

MemberDeclarationSyntax firstMember = root.Members[0];
WriteLine($"The first member is a {firstMember.Kind()}.");
var helloWorldDeclaration = (NamespaceDeclarationSyntax)firstMember;

Anggota itu adalah Microsoft.CodeAnalysis.CSharp.Syntax.NamespaceDeclarationSyntax. Ini mewakili semua yang ada dalam cakupan deklarasi namespace HelloWorld. Tambahkan kode berikut untuk memeriksa node apa yang dideklarasikan di dalam namespace layanan HelloWorld:

WriteLine($"There are {helloWorldDeclaration.Members.Count} members declared in this namespace.");
WriteLine($"The first member is a {helloWorldDeclaration.Members[0].Kind()}.");

Jalankan program untuk melihat apa yang telah Anda pelajari.

Sekarang setelah Anda mengetahui bahwa deklarasi tersebut adalah Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax, deklarasikan variabel baru dari jenis tersebut untuk memeriksa deklarasi kelas. Kelas ini hanya berisi satu anggota: metode Main. Tambahkan kode berikut untuk menemukan metode Main, dan masukkan ke Microsoft.CodeAnalysis.CSharp.Syntax.MethodDeclarationSyntax.

var programDeclaration = (ClassDeclarationSyntax)helloWorldDeclaration.Members[0];
WriteLine($"There are {programDeclaration.Members.Count} members declared in the {programDeclaration.Identifier} class.");
WriteLine($"The first member is a {programDeclaration.Members[0].Kind()}.");
var mainDeclaration = (MethodDeclarationSyntax)programDeclaration.Members[0];

Node deklarasi metode berisi semua informasi sintaksis tentang metode tersebut. Mari kita tampilkan jenis kembalian metode Main, jumlah dan jenis argumen, dan teks isi metode. Tambahkan kode berikut:

WriteLine($"The return type of the {mainDeclaration.Identifier} method is {mainDeclaration.ReturnType}.");
WriteLine($"The method has {mainDeclaration.ParameterList.Parameters.Count} parameters.");
foreach (ParameterSyntax item in mainDeclaration.ParameterList.Parameters)
    WriteLine($"The type of the {item.Identifier} parameter is {item.Type}.");
WriteLine($"The body text of the {mainDeclaration.Identifier} method follows:");
WriteLine(mainDeclaration.Body?.ToFullString());

var argsParameter = mainDeclaration.ParameterList.Parameters[0];

Jalankan program untuk melihat semua informasi yang Anda temukan tentang program ini:

The tree is a CompilationUnit node.
The tree has 1 elements in it.
The tree has 4 using statements. They are:
        System
        System.Collections
        System.Linq
        System.Text
The first member is a NamespaceDeclaration.
There are 1 members declared in this namespace.
The first member is a ClassDeclaration.
There are 1 members declared in the Program class.
The first member is a MethodDeclaration.
The return type of the Main method is void.
The method has 1 parameters.
The type of the args parameter is string[].
The body text of the Main method follows:
        {
            Console.WriteLine("Hello, World!");
        }

Metode kueri

Selain melintasi pohon, Anda juga dapat menjelajahi pohon sintaks menggunakan metode kueri yang ditentukan pada Microsoft.CodeAnalysis.SyntaxNode. Metode ini harus segera akrab bagi siapa pun yang akrab dengan JalurX. Anda dapat menggunakan metode ini dengan LINQ untuk menemukan sesuatu dengan cepat di pohon. SyntaxNode memiliki metode kueri seperti DescendantNodes, AncestorsAndSelf dan ChildNodes.

Anda dapat menggunakan metode kueri ini untuk menemukan argumen ke metode Main sebagai alternatif untuk menavigasi pohon. Tambahkan kode berikut ke bagian bawah metode Main Anda:

var firstParameters = from methodDeclaration in root.DescendantNodes()
                                        .OfType<MethodDeclarationSyntax>()
                      where methodDeclaration.Identifier.ValueText == "Main"
                      select methodDeclaration.ParameterList.Parameters.First();

var argsParameter2 = firstParameters.Single();

WriteLine(argsParameter == argsParameter2);

Pernyataan pertama menggunakan ekspresi LINQ dan metode DescendantNodes untuk menemukan parameter yang sama seperti pada contoh sebelumnya.

Jalankan program, dan Anda dapat melihat bahwa ekspresi LINQ menemukan parameter yang sama seperti menavigasi pohon secara manual.

Contoh menggunakan pernyataan WriteLine untuk menampilkan informasi tentang pohon sintaks saat dilintasi. Anda juga dapat mempelajari lebih banyak lagi dengan menjalankan program yang telah selesai di bawah debugger. Anda dapat memeriksa lebih banyak properti dan metode yang merupakan bagian dari pohon sintaks yang dibuat untuk program halo dunia.

Walker sintaks

Seringkali Anda ingin menemukan semua node dari jenis tertentu di pohon sintaks, misalnya, setiap deklarasi properti dalam file. Dengan memperluas kelas Microsoft.CodeAnalysis.CSharp.CSharpSyntaxWalker dan mengganti metode VisitPropertyDeclaration(PropertyDeclarationSyntax), Anda memproses setiap deklarasi properti di pohon sintaks tanpa mengetahui strukturnya sebelumnya. CSharpSyntaxWalker adalah jenis CSharpSyntaxVisitor tertentu yang secara rekursif mengunjungi node dan setiap turunannya.

Contoh ini mengimplementasikan CSharpSyntaxWalker yang memeriksa pohon sintaks. Ia mengumpulkan direktif using yang ditemukannya yang tidak mengimpor namespace layanan System.

Buat proyek C# Stand-Alone Code Analysis Tool baru; beri nama "SyntaxWalker."

Anda dapat melihat kode yang telah selesai untuk contoh ini di repositori GitHub kami. Contoh di GitHub berisi kedua proyek yang dijelaskan dalam tutorial ini.

Seperti pada contoh sebelumnya, Anda dapat menentukan konstanta string untuk menampung teks program yang akan Anda analisis:

        const string programText =
@"using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace TopLevel
{
    using Microsoft;
    using System.ComponentModel;

    namespace Child1
    {
        using Microsoft.Win32;
        using System.Runtime.InteropServices;

        class Foo { }
    }

    namespace Child2
    {
        using System.CodeDom;
        using Microsoft.CSharp;

        class Bar { }
    }
}";

Teks sumber ini berisi direktif using yang tersebar di empat lokasi berbeda: tingkat file, di namespace layanan tingkat atas, dan di dua namespace layanan bersarang. Contoh ini menyoroti skenario inti untuk menggunakan kelas CSharpSyntaxWalker ke kode kueri. Akan merepotkan untuk mengunjungi setiap node di pohon sintaks root untuk menemukan menggunakan deklarasi. Sebagai gantinya, Anda membuat kelas turunan dan mengganti metode yang dipanggil hanya ketika node saat ini di pohon adalah menggunakan direktif. Pengunjung Anda tidak melakukan pekerjaan apa pun pada jenis node lainnya. Metode tunggal ini memeriksa setiap pernyataan using dan membangun kumpulan namespace layanan yang tidak ada dalam namespace layanan System. Anda membuat CSharpSyntaxWalker yang memeriksa semua pernyataan using, tetapi hanya pernyataan using.

Sekarang setelah Anda menentukan teks program, Anda perlu membuat SyntaxTree dan mendapatkan akar dari pohon itu:

SyntaxTree tree = CSharpSyntaxTree.ParseText(programText);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();

Selanjutnya, buat kelas baru. Di Visual Studio, pilih Proyek>Tambahkan Item Baru. Dalam dialog Tambahkan Item Baru ketik UsingCollector.cs sebagai nama file.

Anda menerapkan fungsi pengunjung using di kelas UsingCollector. Mulailah dengan membuat kelas UsingCollector berasal dari CSharpSyntaxWalker.

class UsingCollector : CSharpSyntaxWalker

Anda memerlukan penyimpanan untuk menampung node namespace layanan yang Anda kumpulkan. Deklarasikan properti baca-saja publik di kelas UsingCollector; Anda menggunakan variabel ini untuk menyimpan UsingDirectiveSyntax node yang Anda temukan:

public ICollection<UsingDirectiveSyntax> Usings { get; } = new List<UsingDirectiveSyntax>();

Kelas dasar, CSharpSyntaxWalker mengimplementasikan logika untuk mengunjungi setiap node di pohon sintaks. Kelas turunan menimpa metode yang dipanggil untuk node tertentu yang Anda minati. Dalam hal ini, Anda tertarik dengan direktif using apa pun. Itu berarti Anda harus mengganti metode VisitUsingDirective(UsingDirectiveSyntax). Satu argumen untuk metode ini adalah objek Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax. Itu keuntungan penting untuk menggunakan pengunjung: mereka memanggil metode yang diganti dengan argumen yang sudah diberikan ke jenis node tertentu. Kelas Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax memiliki properti Name yang menyimpan nama namespace layanan yang diimpor. Ini adalah Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax. Tambahkan kode berikut di ambil alih VisitUsingDirective(UsingDirectiveSyntax):

public override void VisitUsingDirective(UsingDirectiveSyntax node)
{
    WriteLine($"\tVisitUsingDirective called with {node.Name}.");
    if (node.Name.ToString() != "System" &&
        !node.Name.ToString().StartsWith("System."))
    {
        WriteLine($"\t\tSuccess. Adding {node.Name}.");
        this.Usings.Add(node);
    }
}

Seperti contoh sebelumnya, Anda telah menambahkan berbagai pernyataan WriteLine untuk membantu memahami metode ini. Anda dapat melihat kapan itu dipanggil, dan argumen apa yang diteruskan ke sana setiap kali.

Terakhir, Anda perlu menambahkan dua baris kode untuk membuat UsingCollector dan membuatnya mengunjungi node akar, mengumpulkan semua pernyataan using. Kemudian, tambahkan perulangan foreach untuk menampilkan semua pernyataan using yang ditemukan pengumpul Anda:

var collector = new UsingCollector();
collector.Visit(root);
foreach (var directive in collector.Usings)
{
    WriteLine(directive.Name);
}

Kompilasi dan jalankan program. Anda akan menemukan output berikut:

        VisitUsingDirective called with System.
        VisitUsingDirective called with System.Collections.Generic.
        VisitUsingDirective called with System.Linq.
        VisitUsingDirective called with System.Text.
        VisitUsingDirective called with Microsoft.CodeAnalysis.
                Success. Adding Microsoft.CodeAnalysis.
        VisitUsingDirective called with Microsoft.CodeAnalysis.CSharp.
                Success. Adding Microsoft.CodeAnalysis.CSharp.
        VisitUsingDirective called with Microsoft.
                Success. Adding Microsoft.
        VisitUsingDirective called with System.ComponentModel.
        VisitUsingDirective called with Microsoft.Win32.
                Success. Adding Microsoft.Win32.
        VisitUsingDirective called with System.Runtime.InteropServices.
        VisitUsingDirective called with System.CodeDom.
        VisitUsingDirective called with Microsoft.CSharp.
                Success. Adding Microsoft.CSharp.
Microsoft.CodeAnalysis
Microsoft.CodeAnalysis.CSharp
Microsoft
Microsoft.Win32
Microsoft.CSharp
Press any key to continue . . .

Selamat! Anda telah menggunakan Syntax API untuk menemukan jenis pernyataan dan deklarasi C# tertentu dalam kode sumber C#.