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.
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żę.
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.
Zaglądając do instancji klasy EmitResult widzę, że ta kompilacja nie zakończyła się powodzeniem.
Wewnątrz obiektu Diagnostics mogę zobaczyć wszystkie błędy i ostrzeżenia
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.
Jak widać klasa “MetadataFileReference” nie jest już używana. Ma ona atrybut Obsolete i jest ona dostępna tylko wobec swojej biblioteki.
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.
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.
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.
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.
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 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”.
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.