Wtyczki do ReSharper 4.x – Odc. 3 – Analiza kodu i podświetlanie
Jednym z głównych zadań ReSharpera jest analiza kodu i dostarczanie sugestii oraz rozwiązań dla znalezionych problemów. Otwierając plik z kodem naszego programu widzimy jak R# go analizuje - widoczna jest taka strzałka na pasku (ang. stripe) jak po lewej. W tym momencie działa w tle osobny wątek, który nazywa się ‘Daemon’. To w tym wątku wykonywane są wszystkie analizy. Proces analizowania pliku składa się z etapów (ang. stage). R# pozwala na dołączanie własnych etapów do tego procesu i w tym odcinku pokażę właśnie jak to zrobić.
Zadanie jest proste i sprowadza się do zaimplementowania dwóch klas – Daemon Stage oraz Daemon Stage Process. Pierwsza z nich jest odpowiedzialna za utworzenie drugiej, mówiąc wprost pierwsza jest punktem wejścia, dzięki któremu R# będzie mógł dołączyć nasz etap do całej analizy. Zacznijmy od implementacji Daemon Stage.
1: [DaemonStage]
2: public class SharpedDaemonStage : CSharpDaemonStageBase
3: {
4: public override IDaemonStageProcess CreateProcess(IDaemonProcess process)
5: {
6: if (process == null) return null;
7: if (IsSupported(process.ProjectFile) == false) return null;
8:
9: return new SharpedDaemonStageProcess(process);
10: }
11:
12: public override ErrorStripeRequest NeedsErrorStripe(IProjectFile projectFile)
13: {
14: return ErrorStripeRequest.STRIPE_AND_ERRORS;
15: }
16: }
Poprawna implementacja Daemon Stage wymaga dwóch elementów. Po pierwsze zaimplementować należy interfejs IDaemonStage oraz klasę oznaczyć atrybutem DaemonStageAttribute. W powyższym przykładzie poszliśmy trochę dalej i wykorzystaliśmy klasę bazową CSharpDaemonStageBase, która dostarcza nam metodę pomocniczą IsSupported, sprawdzającą czy analizowany plik jest plikiem C# (ciekawskich odsyłam do Reflectora). Tak na marginesie to bez Reflectora praca nad jakąkolwiek wtyczką byłaby niemożliwa i jest to narzędzie, z którego korzystam przez 80% pisania wtyczki. Wracając do powyższej implementacji jasnym jest już chyba, że zadaniem metody CreateProcess jest dostarczenie instancji naszego procesu, który będzie analizował plik. Zaimplementujemy go za chwilę.
Zatrzymajmy się jeszcze przy drugiej metodzie – NeedsErrorStripe. Jej zadaniem jest określenie podstawowych właściwości procesu. Do wyboru mamy trzy wartości: None, Stripe oraz Stripe_and_Errors. Pierwsza z nich oznacza, ze proces w ogóle nie będzie korzystał z ***. Druga – pasek jest potrzebny, ale proces nie będzie produkował żadnych ostrzeżeń i błędów. Natomiast ostatnia – pasek jest potrzebny i proces będzie produkował ostrzeżenia i błędy.
Teraz zaimplementujemy sam proces.
1: internal class SharpedDaemonStageProcess : CSharpDaemonStageProcessBase
2: {
3: public SharpedDaemonStageProcess(IDaemonProcess daemonProcess)
4: : base(daemonProcess)
5: {
6: }
7:
8: public override void ProcessFile(ICSharpFile file)
9: {
10: file.ProcessDescendants(this);
11: this.FullyRehighlighted = true;
12: }
13:
14: public override void VisitThrowStatement(IThrowStatement throwStatementParam)
15: {
16: this.AddHighlighting(new ThrowHighlighting(throwStatementParam));
17: }
18: }
Ograniczyłem się tu do zaprezentowania samego mechanizmu podświetlania, nie zaś analizy. Jedynym zadaniem powyższej implementacji jest podkreślenie wszystkich słów kluczowych throw.
Zanim jednak przejdę do omówienia tej implementacji chciałbym pokrótce omówić to jak R# działa (przynajmniej jak ja to rozumiem). Otóż z tego co wiem, to R# posiada swój własny parser języka C# (to tłumaczy trochę czas jaki potrzebują na adaptację do nowej wersji języka). Wynikiem działania tego parsera jest drzewo PSI, które możemy następnie analizować. Proces analizy w R# został oparty o wzorzec Wizytatora (ang. Visitor)
. Dlatego chcąc przeanalizować strukturę drzewa PSI należy uruchomić własnego wizytatora na tym drzewie. W powyższym przykładzie dziedziczymy po klasie CSharpDaemonStageProcessBase. Jest to klasa bazowa dla etapów działających dla języka C#. Na obrazku po pawej mamy hierarchię reprezentującą tę klasę. Jak łatwo zauważyć dziedziczy ona po klasie bazowej ElementVisitor, zatem jest wizytatorem elementów drzewa (każdy węzeł w drzewie implementuje interfejs IElement). Następnie mamy IDaemonStageProcess, a więc będzie też etapem. Nawiązując jeszcze do klasy ElementVisitor trzeba zaznaczyć, że definiuje ona szereg metod pozwalających odwiedzać poszczególne elementy drzewa. 
Analizę zaczynamy wywołując metodę ProcessDescendants na przekazanym nam pliku. W trakcie analizy nasz etap może dołączać swoje podświetlenia za pomocą metody AddHighlighting. Po zakończeniu etap przekazuje wszystkie podświetlenia oraz zakres dokumentu jaki został przeanalizowany (robi to za nas klasa bazowa). Zwykle jest to cały plik, dlatego także w naszym przypadku ustawiamy właściwość FullyRehighlighted na true. Możliwe są też bardziej zaawansowane scenariusze, w których analizie może podlegać tylko pewien fragment drzewa. Pozwala to uzyskać lepszą wydajność, jedka ja jeszcze nie znam szczegółów. 
Przed przystąpieniem do analizy drzewa PSI należy poznać jego strukturę. Jest to drzewo obiektowo reprezentujące strukturę kodu, zatem każdemu elementowi odpowiada pewna klasa. W naszym przykładzie chcemy analizować słowa kluczowe throw, a więc będziemy analizowali throw statements. Obrazek obok prezentuje tą właśnie klasę oraz całą hierarchię dziedziczenia. W tym miejscu należy podkreślić dwie ważne prawidłowości, które dotyczą wszystkich elementów. Każdy z nich implementuje dwa interfejsy. W naszym przypadku będą to IThrowStatement oraz IThrowStatementNode. Pierwszy z nich reprezentuje logiczny element, w tym przypadku rzucenie wyjątku za pomocą słowa kluczowego throw. Drugi z nich reprezentuje ten sam obiekt, ale w realiach drzewa PSI. Co więcej zauważmy, że IThrowStatementNode dziedziczy po IThrowStatement. Dla mnie na początku było to bardzo mylące i nie do końca łapałem strukturę. Szczególnie, gdy implementując wizytatora dostajemy np. interfejs IThrowStatement i patrząc na jego zawartość poprzez intellisense nie mamy elementów, których byśmy się spodziewali. Na szczęście każdy tego typu element możemy rzutować na odpowiadający mu typ *Node, albo najzwyczajniej wywołać na nim metodę ToTreeNode.
O ile interfejs logiczny dostarcza nam elementów składowych np. dla throw mamy właściwość Exception dającą nam dostęp do wyrażenia reprezentującego rzucany wyjątek, o tyle interfejs drzewkowy daje nam dostęp do poszczególnych elementów takich jak słowo kluczowe throw, wyjątek czy średnik.
Wracając do naszego przykładu, w którym podkreślamy słowa kluczowe throw widać, że implementujemy metodę VisitThrowStatement. Ta metoda zostanie wywołana dla każdego słowa kluczowego throw znajdującego się w analizowanym pliku. Nie przeprowadzamy tu żadnej analizy, po prostu dodajemy podświetlenie.
Podświetlenia (ang. highlightings) definiowane są jako dedykowane klasy dziedziczące po CSharpHighlightingBase i implementujące interfejs IHighlighting. Poniżej przykładowa implementacja.
1: [StaticSeverityHighlighting(Severity.WARNING)]
2: public class ThrowHighlighting : CSharpHighlightingBase, IHighlighting
3: {
4: private IThrowStatement ThrowStatement { get; set; }
5:
6: public ThrowHighlighting(IThrowStatement throwStatement)
7: {
8: ThrowStatement = throwStatement;
9: }
10:
11: public override DocumentRange Range
12: {
13: get { return this.ThrowStatement.ToTreeNode().ThrowKeyword.GetDocumentRange(); }
14: }
15:
16: public string ErrorStripeToolTip
17: {
18: get { return "This is throw statement! (on a strip)"; }
19: }
20:
21: public int NavigationOffsetPatch
22: {
23: get { return 0; }
24: }
25:
26: public string ToolTip
27: {
28: get { return "This is throw statement! (tool tip)"; }
29: }
30: }
Przeanalizujmy teraz ten przykład. W konstruktorze przyjmujemy IThrowStatement bo to będzie dla nas źródło danych. Następnie z klasy bazowej mamy do zaimplementowania abstrakcyjną właściwość Range. Właściowść ta powinna zwrócić obiekty typu DocumentRange reprezentujący zakres na drzewie, który powinien zostać podświetlony. Każdy element drzewa ma metodę GetDocumentRange(), która zwraca jego zakres na drzewie. Z tego co się orientuję to jest jeszcze zakres tekstowy, odpowiadający pozycji w edytorze i można go pobrać z właściwości TextRange obiektu DocumentRange. Jako, że w przykładzie chcemy podświetlać słowa kluczowe throw, DocumentRange pobieramy z właściwości ThrowKeyword.
W kolejnym kroku mamy za zadanie zwrócić odpowiedni opis dla naszego podświetlenia. Mamy trzy miejsca, w których opis będzie widoczny. Jest to boczny pasek ze znacznikami i po najechaniu znacznika reprezentującego dane podświetlenie wyświetli się zawartość ErrorStripeToolTip. Natomiast po najechaniu myszką na podświetlenie lub ustawienie tam kursora spowoduje wyświetlenie właściwości ToolTip.
Ostatnim elementem i chyba najrzadziej używanym jest NavigationOffsetPatch. Jest to offset (tekstowy), o który zostanie przesunięty kursor podczas nawigowania do tego podświetlenia. Co ciekawe zauważyłem, że offset ten zostanie zaaplikowany jak będziemy nawigować za pomocą komend np. „Go to next highlighting” natomiast nie jak klikniemy na pasek reprezentujący podświetlenie. Przykładowo w naszym przykładzie zwrócenie wartości 5 zaowocowałoby ustawieniem kursora za słowem kluczowym throw. Ciekawskich zachęcam do eksperymentów.
Ostatnim elementem jest atrybut znajdujący się nad klasą – StaticSeverityHighlighting. Atrybut ten definiuje nam podświetlenie, którego severity jest statyczne i nie można go modyfikować w opcjach. Konfigurowalnych podświetleń jeszcze nie rozczaiłem.
Takim oto sposobem mamy gotowe rozwiązanie podświetlające wszystkie słowa kluczowe throw. Nie jest to co prawda bardzo użyteczna analiza jednak mam nadzieję, że przykład dostatecznie pokazał Wam mechanizm podświetlania w R#. W złączniku znajduje się cały ten dodatek, który możecie sobie sami uruchomić i trochę z nim poeksperymentować. Pamiętajcie, aby w opcjach debugowania projektu ustawić odpowiednią ścieżkę do pliku z wtyczką tak jak to zostało opisane w części drugiej.
-
-
-
Analiza kodu i podświetlanie (ten tekst)
-
R#4.5 Beta i errata do trzeciego odcinka