EmitCzęść NR.3

Wewnątrz kompilatora Roslyn oczywiście istnieją klasy, które na podstawie podanego kodu potrafią utworzyć biblioteki.

Są one znakomitą alternatywą dla System.Reflection.Emity  i CodeDOM.

Generowanie kodu jeszcze nie było takie łatwe.

Klasy kompilujące potrafią także bardzo szczegółowo poinformować użytkownika, dlaczego dany kod się nie skompilował.

Klasy te także analizują kod pod względem zasad. Sprawdzają przykładowo, czy zmienne prywatne zaczynają się od znaku podkreślenia i są napisane z małej litery.

Składnia, a zasady czytelności

Alternate Text

Czym jest składnia? Jest to tekstowa reprezentacja kodu źródłowego danego języka programowania. Przestrzega ona zasady gramatycznej języka.

W składni języka C# komentarze, spacje i formatowanie nie mają znaczenia.

Składnia C# zastrzega sobie pewne słowa i używa ich jako słów kluczowych. W składni najważniejsza jest prawidłowość kodu. Dla składni i dla maszyny cały kod mógłby być bez białych znaków.

Zasady czytelności natomiast określa zrozumienie kodu.  Kod jest czytany przez ludzi i powinien być dla nich zrozumiały. Programista powinien rozumieć, co obecny kod robi wedle konkretnego kontekstu.

Kompilator naturalnie analizuje składnie kodu i określa, czy jest on prawidłowy. Kod będzie się oczywiście nadal kompilował, ale sam kompilator w takim wypadku zwróci ostrzeżenia.

Dlatego od Visual Studio 2015 pojawiają się ostrzeżenia kompilatora, co do wielkości znaków przy deklaracji zmiennych prywatnych. Wcześniej taka funkcjonalność istniała dopiero po zainstalowaniu dodatku Resharper.

Mając też instancje kompilatora z kodem możemy analizować kod od strony semantycznej.

Możemy więc przykładowo  odpowiedzieć sobie na pytanie, czy dana klasa może być rzutowana na inną.

Co w tym wpisie też pokażę.

Składnia a semantyka

Alternate Text

Składnia kodu jest analizowana bez potrzeby kompilacji kodu, co wykazałem używając Syntax Tree API. Do analizy składni musimy znać tylko gramatykę danego języka programowania.

Semantyczna analiza potrzebuje większego kontekstu. Co ten symbol zwróci. Czy dana metoda lub klasa istnieje w obecnym kontekście, czyli istnieje referencja i istnieje zapis using.

Czy dana instancja klas może być rzutowana na inną. . .itp.

Bez kompilacji taka analiza jest niemożliwa.

Skoro do kompilacji naszego kodu wystarczą te klasy oznacza to, że nie musimy już uruchamiać polecenia konsolowego “csc.exe”.

Wszystkie polecenia“csc.exe” zostały przeniesione na klasy i metody.

Role nawet się odwróciły, ponieważ klasy Roslyn nie wywołującsc.exe, tylko to .csc.exe wywołuje klasy Roslyn.

Mamy więc obiektowy model kompilatora kodu, który możemy konfigurować za pomocą klas i metod.

Klasy kompilatora też modelują projekt kodu. Bez dodania referencji niczego nie skompilujemy

Skompilujmy kod

Kompilator przyjmuje kod w postaci drzewa.  Oczywiście używając Syntax API jestem w stanie utworzyć obiekt drzewa, na postawie kodu w postaci tekstowej.

Do klasy “CSharpCompilation” jestem w stanie dodać wiele drzew kodu. Obecnie dodaje tylko jedno.

var tree = CSharpSyntaxTree.ParseText("class Klasa {void Hello() {}");

var comp = CSharpCompilation.Create("MyDemoCode")
    .AddSyntaxTrees(tree)
var result = comp.Emit("MyDemoCode.exe");

Jeśli chcę skompilować kod na instancji klasy kompilatora, wywołuje metodę Emit.

W przypadku błędnej kompilacji nie pojawi się żaden wyjątek.

EmitResult

Zaglądając do instancji klasy EmitResult widzę, że ta kompilacja nie zakończyła się powodzeniem.

EmitResult 2

Wewnątrz obiektu Diagnostics mogę zobaczyć wszystkie błędy i ostrzeżenia

EmitResult 3

Rozwiążmy więc pierwszy problem z naszym kodem. Jak widzimy kompilator nawet nie wie, czym jest typ Object i czym jest typ void.

Brakuje mu  referencji do biblioteki “mscorlib”.

Jak więc tę referencje dodać.

Sprawa się bardzo zmieniła i w Internecie można zobaczyć dużo nieaktualnych przykładów.

Klasa MetadataFileReference jest poza moim zasięgiem, chociaż istnieje.

MetadataFileReference

Jak widać klasa “MetadataFileReference” nie jest już używana. Ma ona atrybut Obsolete i jest ona dostępna tylko wobec swojej biblioteki.

image

Na szczęście ten wpis na stackoverflow dobrze mnie nakierował. Mam nadzieje, że to API nie będzie się już zmieniać.

http://stackoverflow.com/questions/26962337/metadatafilereference-is-inaccessible

Używając klasy MetadataReference dodaję informacje o referencji do biblioteki “mscorelib”.

var tree = CSharpSyntaxTree.ParseText("class Klasa {void Hello() {}");

var mscorlib = MetadataReference.
    CreateFromAssembly(typeof(object).Assembly);

var comp = CSharpCompilation.Create("MyDemoCode")
    .AddSyntaxTrees(tree)
    .AddReferences(mscorlib);

var result = comp.Emit("MyDemoCode.exe");

Czy mając to wszystko skompiluje kod?

Oczywiście, że nie.

W kodzie są jeszcze dwa błędy.

EmitResult 4

Jeden błąd wynika z braku jednego nawiasu klamrowego. Łatwo to poprawić.

Drugi błąd jest specyficzny dlacsc.exe.

Domyślnie csc.exe jak klasa kompilująca wypluwa plik .exe, a on oczekuje na istnienie metody głównej “Main”.

Do klasy kompilującej muszę więc dodać informację o tym, że chcę kod skompilować, jako bibliotekę.

var tree = CSharpSyntaxTree.ParseText("class Klasa {void Hello() {} }");

var mscorlib = MetadataReference.
    CreateFromAssembly(typeof(object).Assembly);


var options = new CSharpCompilationOptions
    (OutputKind.DynamicallyLinkedLibrary);

var comp = CSharpCompilation.Create("MyDemoCode")
    .AddSyntaxTrees(tree)
    .AddReferences(mscorlib)
    .WithOptions(options);
var result = comp.Emit("MyDemoCode.dll");

Kompilacja przebiegła pomyślnie.

EmitResult 5

Używając aplikacji dotPeek mogę podejrzeć do dll-ki, którą właśnie utworzyłem i zobaczyć, że rzeczywiście znajdują się w niej klasy, które napisałem w formie tekstowej.

 dotPeek

Co, jeśli mam wiele plików tekstowych z kodem?

Patrząc na metody klasy instancji kompilatora, w przypadku posiadania wielu plików z kodem źródłem, po prostu dodajesz kolejne wyrażenia drzewiaste do kompilatora.

jghjhjhjh3

Semantyczna analiza kodu

W pliku code.txt mam następujący kod.

using System;

public class Product
{

}

public class Movie : Product
{

}

public class TShirt : Product
{

}

public class ShopingList
{
    public void ShowMovie(Movie d)
    {

    }

    public void ShowMovie(TShirt d)
    {

    }

    public static explicit operator ShopingList(string s)
    { throw new NotImplementedException(); }

    public static implicit operator string (ShopingList x)
    { throw new NotImplementedException(); }

}

Kod tworzący instancje obiektu kompilatora wygląda tak.

string code = "";

using (StreamReader sr = new StreamReader("Code.txt"))
{
    code = sr.ReadToEnd();
}

var tree = CSharpSyntaxTree.ParseText(code);
var mscorlib = MetadataReference.
CreateFromAssembly(typeof(object).Assembly);


var options = new CSharpCompilationOptions
(OutputKind.DynamicallyLinkedLibrary);

var comp = CSharpCompilation.Create("MyDemoCode")
.AddSyntaxTrees(tree)
.AddReferences(mscorlib)
.WithOptions(options);

Teraz mając instancje kompilatora mogę sprawdzić, czy dany typ może być skonwertowany na inny.

var conv = comp.ClassifyConversion(
comp.GetSpecialType(SpecialType.System_Int32),
comp.GetSpecialType(SpecialType.System_Object));

var conv2 = comp.ClassifyConversion(
comp.GetSpecialType(SpecialType.System_Object),
comp.GetSpecialType(SpecialType.System_Int32));


var conv3 = comp.ClassifyConversion(
comp.GetSpecialType(SpecialType.System_Object),
comp.GetTypeByMetadataName("Movie"));


var conv4 = comp.ClassifyConversion(
comp.GetTypeByMetadataName("Product"),
comp.GetTypeByMetadataName("Movie"));


var conv5 = comp.ClassifyConversion(
 comp.GetSpecialType(SpecialType.System_String),
comp.GetTypeByMetadataName("ShopingList"));

var conv6 = comp.ClassifyConversion(
comp.GetTypeByMetadataName("ShopingList"),
comp.GetSpecialType(SpecialType.System_String));

Otrzymam w ten sposób informację, w jaki sposób jeden typ zostanie zmieniony na inny.

Typ int na object zostanie zmieniony na podstawie operacji “Boxing”.

Typ object na int zostanie zmieniony na podstawie operacji “UnBoxing”.

Każda klasa jest obiektem, więc moja klasa “Movie” zostanie zamieniona na typ object, w wyniku operacji jawnej referencji.

Klasa Movie dziedziczy po klasie produkt, więc ona też może być poddana takiej operacji.

Konwersja

Konwersja typu string na ShopingList zadziała tylko dlatego, że utworzyłem odpowiednie operatory jawnej i niejawnej konwersji.

Wszystkie te informacje mogę uzyskać używając instancji kompilatora i metody “ClassifyConversion”.

MethodSymbol

Jak widać przeglądając wszystkie właściwości instancji klasy “Conversion” mogę uzyskać jeszcze więcej informacji. Mogę się dowiedzieć jaka metoda zostanie wywołana w przypadku tej niejawnej konwersji.

To tyle jeśli chodzi o analizę kodu przy użyciu instancji kompilatora. By analizować kod pod względem jeszcze innych właściwości, trzeba połączyć Syntax Tree API z API kompilacyjnym oraz użyć semantycznego modelu.