Roslyn Kompilator .NET : csc.exe i pierwsze rozszerzenie fix

Kompilatory C# i Visual Basic były napisane w C++. Co jest logiczne. Bo C# i Visual Basic były nowymi językami. Nie możesz napisać kompilatora nowego języka w samym języku, który dopiero się tworzy.

Wybór ten jednak stworzył pewien problem.  Ekipa pracująca nad językiem C# musiała też znać C++.

Aby dodać nową funkcjonalność języka C# lub Visual Basic trzeba było się mocno wysilić.

Kompilatory nie mogły używać swojego języka.

Ile mamy kompilatorów

Mam  do dyspozycji polecenia, dzięki którym możemy skompilować kod bez korzystania z Visual Studio.

  • Dla C# csc.exe
  • Dla Visual Basic vbc.exe

W samym Visual Studio istnieje też kompilator działający w tle. Ma on za zadanie powiedzieć nam, co jest nie tak w naszym w kodzie, zanim skompilujemy kod. Kompilator ten działa z mniejszą częstotliwością i jest tolerancyjny do niekompletnego kodu, który właśnie uzupełniamy.

W Visual Studio istnieje także mały okienkowy kompilator „Immediate Window”. Pozwala on na uruchomienie kodu w trakcie debugowania kodu. Kompilator w „Immediate Window” korzysta tylko z niektórych wyrażeń języka. Przykładowo w nim nie można używać wyrażeń LINQ.

Dlaczego powstał więc kompilator Roslyn i dlaczego został wydany na świat?

Wraz z C# 6.0 można zauważyć dużo testów i prototypownia  nowych funkcjonalności. W tej edycji pojawiło się dużo drobnych, miłych funkcji w samym języku.  Normalnie nie było to możliwe, bo budowa kompilatora to poważna sprawa i dodanie nawet najmniejszej funkcji liczy się z wieloma problemami. Powstanie kompilatora Roslyn na pewno ułatwiło wielu pomysłodawcom języka osobom życie.

Jednym z celów kompilatora Roslyn było zbudowanie bogatego API opartego:

  • Na drzewach składniowych
  • Semantycznej analizie
  • Modelu kompilacyjnym

Co to wszystko znaczy, Zajrzymy do projektów rozszerzających kompilator i się przekonamy.

Roslyn też rozwiązuje problem związany z posiadaniem wielu wersji kompilatorów. Mamy kompilator działający w tle Visual Studio i  np. kompilator, który działa w „Immediate Window”.

Powinien być jeden kompilator.

Roslyn jest także otwarty, czyli pozwala na użycie dodatków od firm trzecich. Eliminuje to prywatność tego API jak wielu innych API w Visual Studio. Wiele innych edytorów powinno też mieć możliwość  korzystania z kompilatora Roslyn  w swoim własnym środowisku, niekoniecznie w systemie Windows.

Teraz, ponieważ cały kod kompilatora Roslyn jest dostępny, oznacza to, że każdy może podejrzeć kod działania kompilatora, co tworzy wiele możliwości edukacyjnych.

Roslyn jest więc oprogramowaniem Open Source.

Koniec z czarnymi skrzynkami kompilatora.

Kompilator

csc.exe

Co tak naprawdę dzieje się w środku kompilatora. Kompilator analizuje tekst w tym wypadku pliki .cs. Kompilator analizują kod, patrzy na symbole oraz na wyrażenia importujące działania z innych bibliotek.

Później kompilator robi powiązania i analizuje zmienne. Pod koniec tego procesu kompilator przerobi kod, na inny kod, który nazywa się IL, a on zapisuje się do biblioteki dll.

W wielkim skrócie tak właśnie działa kompilator.

Teraz możemy zajrzeć do skrzynek i nawet je przerobić do swoich własnych potrzeb.

Kompilator Roslyn jest napisany w C#, co oznacza, że każdy krok działania kompilatora został przetłumaczony na klasy i metody.

Co ma sens. Bo API kompilatora jak można się, domyśleć musi, być skomplikowane, w końcu nawet analiza tego prostego kodu może być łamaniem głowy.

int i = 12;
Console.WriteLine(i);

Każdy element tego kodu jest pewnym wyrażeniem, które musi być przeanalizowane.

Nawet przez kompilator działający w tle.

W końcu na jakiejś podstawie Visual Studio koloruje nam składnie kodu. Na jakiejś podstawie Visual Studio formatuje kod.

Syntax Tree

Wewnątrz kompilatora Roslyn mamy więc „Syntax Tree API”, który właśnie analizuje składnię tekstową i układa to wszystko w strukturę drzewiastą.

Syntax Tree 2

Kompilator Roslyn ma też „Symbol API”, który nam powie, że wyrażenie „Console” pochodzi od innej biblioteki. Na bazie tego API Visual Studio ma funkcjonalność „Navigate to”.

Przycisk F12, który nawiguje nas do kodu danej klasy.

Na tym oczywiście zabawa się nie kończy. Potrzebne jest api do analizowania powiązań zmiennych i przepływu kodu. Od tego jest wiele pod API, ale można je zgrupować pod nazwą „Binding and Flow Analysis APIs”.

Bez nich nie byłoby mowy o zmianie nazwy klasy w wielu miejscach, w całym kodzie,  w Visual Studio za pomocą naciśnięcia jednego guzika.

W takim wypadku API  sprawdza, gdzie klasa istnieje i zmienia jej nazwę w kodzie. Kompilator jest istotnym narzędziem, bo występuje w wielu miejscach. Nie służy on tylko do wypluwania pliku dll czy exe.

Występuje on w oknie Solucji, w samym projekcie „.sln” i wielu innych funkcjach Visual Studio.

Class

Roslyn też posiada "Emit API", którego celem jest utworzenie kodu IL. Obecnie to API daje możliwość edycji kodu w trakcie debugowania i kontynuowania debugowania po zmianie kodu.

csc.exe co nowego

Zobaczmy, co potrafi kompilator, zaczynając od okna poleceń Visual Studio. Już na tym poziomie możemy zobaczyć, co nowego jest w tym kompilatorze. Co on potrafi.

Wchodzimy do folderu, gdzie są skróty do Visual Studio 2015 Preview i w folderze Visual Studio Tools odnajdujemy „Developer Command Prompt for CS2014”.

Developer Command Prompt for CS2014

Wpisując do konsoli „csc /?” mogę znaleźć wszystkie polecenia związane z kompilatorem Rolsyn.

Jeśli nigdy nie bawiłeś się kompilatorem na poziomie poleceń tekstowych, to jest to dobry początek. Poleceń jest dużo.

csc /?”

Skorzystanie z kompilatora w wierszu poleceń jest dosyć proste. Wystarczy napisać csc i podać nazwę pliku bądź jego listę.

csc.exe test.cs

using System;

class Hello 
{
    static public void Write()
    {
        Console.WriteLine("Hello");
    }

    static public void Main(string[] prams)
    {
        Write();
    }
}

Został on domyślnie skompilowany do pliku exe.

exe

Jeśli chciałbym skompilować plik cs do biblioteki, musiałbym określić parametr target i parametr out określający nazwę pliku dll.

dll

Czy coś nowego jest w kompilatorze na poziomie wiersza poleceń.

Po pierwsze została dodana opcji analizy bibliotek.

Analyzer

Istnieje też możliwość kompilowania plików współbieżnie.

parallel

Jeśli spodobała ci się idea kompilacji kodu z poziomu konsoli. Spójrz na polecenie .MSBuild.exe, które nie współpracuje z plikami CS, ale z projektami Visual Studio. Polecenie te jest przydatne, gdy chcemy skompilować listę projektów Visual Studio bez uruchamiania samego Visual Studio.

Przejdźmy do prawdziwych projektów Roslyn.

Zaczynamy przygodę : pierwsze rozszerzanie Roslyn

Kod źródłowy kompilatora Roslyn. jest dostępne na CodePlex.

https://roslyn.codeplex.com/

Polecam tę stronę Do przeglądania klas  wewnątrz API Roslyn.

http://source.roslyn.codeplex.com/

Aby zacząć zabawę z kompilatorem obecnie trzeba zainstalować Visual Studio 2015 Preview oraz Visual Studio Preview SDK.

Visual Studio 2015 Preview

Do kolorowania składni trzeba zainstalować dodatek .NET Compiler Platform Syntax Visualizer.

https://visualstudiogallery.msdn.microsoft.com/70e184da-9b3a-402f-b210-d62a898e2887

Kolejna ważna rzecz to zbiór projektów, które pomogą nam zrozumieć jak rozszerzać kompilator Roslyn.

https://visualstudiogallery.msdn.microsoft.com/849f3ab1-05cf-4682-b4af-ef995e2aa1a5

Mając, to wszystko jesteś gotowy. W Visual Studio 2015 pod zakładką Extensibility powinieneś znaleźć projekty jak:

  • Code Refactoring VSIX
  • Diagnostics with Code Fix
  • Compiler Platform Console Application

Są to właśnie projekty rozszerzające działania kompilatora Roslyn.

Compiler Platform Console Application

Wybierz projekt „Diagnostics with Code Fix”

Zanim zaczniesz działać, sprawdź, czy posiadasz dodatkowe okno „Roslyn Syntax Visualizer”.

Roslyn Syntax Visualizer

Powinieneś je znaleźć w View –> Other Windows –> Roslyn Syntax Visualizer.

W oknie możesz zobaczyć wyrażenie drzewiaste obecnego pliku .cs.

Roslyn Syntax Visualizer

Daje to trochę do myślenia jak kod C# jest analizowany.  Przykładowo wszystko oznaczone słowem „Trail” nie ma wpływu na działanie kodu. Są to komentarze i białe znaki.

Wyrażenia drzewiaste Roslyn przydadzą się później, na razie piszemy rozszerzenie a la Hello World.

Roslyn Syntax Visualizer

Wróćmy więc do projektu “Diagnostic with Code Fix”.

Diagnostic with Code Fix2

W projekcie są dwa bardzo ważne pliki. Pierwszy z nich to “DiagnosticAnalyzer”

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class Analyzer1Analyzer : DiagnosticAnalyzer
{
    public const string DiagnosticId = "Analyzer1";
    internal const string Title = "Type name contains lowercase letters";
    internal const string MessageFormat = "Type name '{0}' contains lowercase letters";
    internal const string Category = "Naming";

    internal static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true);

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }

    public override void Initialize(AnalysisContext context)
    {
        // TODO: Consider registering other actions that act on syntax instead of or in addition to symbols
        context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);
    }

    private static void AnalyzeSymbol(SymbolAnalysisContext context)
    {
        // TODO: Replace the following code with your own analysis, generating Diagnostic objects for any issues you find
        var namedTypeSymbol = (INamedTypeSymbol)context.Symbol;

        // Find just those named type symbols with names containing lowercase letters.
        if (namedTypeSymbol.Name.ToCharArray().Any(char.IsLower))
        {
            // For all such symbols, produce a diagnostic.
            var diagnostic = Diagnostic.Create(Rule, namedTypeSymbol.Locations[0], namedTypeSymbol.Name);

            context.ReportDiagnostic(diagnostic);
        }
    }
}

Implementuje on klasę abstrakcyjną DiagnosticAnalyzer. Na tej podstawie wiemy, że klasa tak będzie rozszerzać zachowanie analizy symboli w kodzie. A dokładnie mówiąc metoda “AnalyzeSymbol” uruchomi się, gdy najedziemy kursorem myszki np. na nazwę zmiennej.

Klasa ta będzie sprawdzać, czy dany symbol jest napisany małą literą. Jeśli tak jest, wyświetli się nasz komunikat.

Jest to prosty przykład, ale fajnie, że już wiemy, co się dzieje w tym projekcie, mimo iż nie napisaliśmy jeszcze żadnego kodu.

Sprawa się jeszcze tutaj nie kończy, bo po naszej wymyślonej analizie kodu Visual Studio będzie oczekiwać od naszego rozszerzenia podania listy rozwiązań tego problemu.

Do akcji wkracza klasa “CodeFixProvider”.

[ExportCodeFixProvider("Analyzer1CodeFixProvider", LanguageNames.CSharp), Shared]
public class Analyzer1CodeFixProvider : CodeFixProvider
{
    public sealed override ImmutableArray<string> GetFixableDiagnosticIds()
    {
        return ImmutableArray.Create(Analyzer1Analyzer.DiagnosticId);
    }

    public sealed override FixAllProvider GetFixAllProvider()
    {
        return WellKnownFixAllProviders.BatchFixer;
    }

    public sealed override async Task ComputeFixesAsync(CodeFixContext context)
    {
        var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

        // TODO: Replace the following code with your own analysis, generating a CodeAction for each fix to suggest
        var diagnostic = context.Diagnostics.First();
        var diagnosticSpan = diagnostic.Location.SourceSpan;

        // Find the type declaration identified by the diagnostic.
        var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<TypeDeclarationSyntax>().First();

        // Register a code action that will invoke the fix.
        context.RegisterFix(
            CodeAction.Create("Make uppercase", c => MakeUppercaseAsync(context.Document, declaration, c)),
            diagnostic);
    }

    private async Task<Solution> MakeUppercaseAsync(Document document, TypeDeclarationSyntax typeDecl, CancellationToken cancellationToken)
    {
        // Compute new uppercase name.
        var identifierToken = typeDecl.Identifier;
        var newName = identifierToken.Text.ToUpperInvariant();

        // Get the symbol representing the type to be renamed.
        var semanticModel = await document.GetSemanticModelAsync(cancellationToken);
        var typeSymbol = semanticModel.GetDeclaredSymbol(typeDecl, cancellationToken);

        // Produce a new solution that has all references to that type renamed, including the declaration.
        var originalSolution = document.Project.Solution;
        var optionSet = originalSolution.Workspace.Options;
        var newSolution = await Renamer.RenameSymbolAsync(document.Project.Solution, typeSymbol, newName, optionSet, cancellationToken).ConfigureAwait(false);

        // Return the new solution with the now-uppercase type name.
        return newSolution;
    }
}

Visual Studio wtedy w tej klasie wywoła metodę „GetFixesAsync”. Metoda ta zwróci tablice operacji. Obecnie zwraca ona jednak tylko kolekcję jednoelementową składającą się tylko z operacji „MakeUppercaseAsync”.

Operacja ta zmieni małe litery w  napisie tekstowym identyfikatora na duże litery.

Co się stanie, jeśli będziesz chciał zdebugować ten projekt. No bo jak. To nie jest aplikacja przecież. Otóż po naciśnięciu Start Debbuging dzieje się prawdziwa magia.

 Start Debbuging

Uruchamia mi się kolejne Visual Studio i na nim mogę zobaczyć, czy moja nowa funkcjonalność działa poprawnie. W swoim projekcie z rozszerzeniem naturalnie mogę   postawić breakpoint.

To rozszerzenie będzie zaznaczać kod na zielono, gdy w tym wypadku klasa zawiera w sobie małe litery. Po najechaniu kursorem wyświetla mi się możliwość powiększenia wszystkich liter.

Progam

Po zatrzymaniu debugowania Visual Studio, które służyło mi do testowania, znika.

Właśnie napisaliśmy swoje pierwsze rozszerzenie do kompilatora Roslyn.

Rozszerzenie to można później zainstalować. W końcu ten projekt stworzył Instalkę VSIX.

instalke VSIX.

W następnym wpisie przeanalizujemy wyrażenia Syntax Trees w kompilatorze Roslyn.