Zaawansowana analiza i refaktoryzacja kodu z Facebook PFFF

Obrazek użytkownika jsobiecki
Zaawansowana analiza i refaktoryzacja kodu z Facebook PFFF

Wprowadzenie i motywacja

Jestem fanem popularnej w systemach UNIX-owych filozofii, głoszącej że lepiej tworzyć narzędzia rozwiązujące pojedynczy problem w dobry sposób, niż narzędzia służące do rozwiązywania wielu problemów (nie koniecznie optymalnie). Dzięki różnym mechanizmom (np. unix pipes), narzędzia można łączyć ze ze sobą, tak by realizować bardziej skomplikowane zadania.

Dlatego też, jako programista PHP, pewien czas temu obraziłem się na przerośnięte IDE, które często mimo że oferowały dużo funkcji w jednym miejscu, nie realizowały ich wystarczająco dobrze lub kosztem kiepskiej stabilności. Oczywiście czas płynie, jakość tych rozwiązań pewnie też uległa poprawie, ale mnie nie kusi by wejść do tej samej rzeki ponownie. Pewne IDE są też rozwiązaniami własnościowymi, co nie jest może zbrodnią, ale jednak preferuję narzędzia otwarto źródłowe. W moim wypadku VIM z armią drobnych pluginów zintegrowany z zabawkami takimi jak PHP Mess Detector, PHP Code Sniffer, CTags czy też CSCope jest nie do zastąpienia, przynajmniej jeżeli chodzi o pracę z kodem PHP. Niestety, jestem też świadom pewnych braków. Największy problem mam z bardziej skomplikowaną analizą i refaktoryzacją kodu. Mechanizmy podpowiadania kodu także nie są doskonałe. Dlatego szukam narzędzi, które realizują te zadania lepiej, a jednocześnie mają potencjał by być w miarę prosty sposób zintegrowane z dowolnym środowiskiem programistycznym (w moim przypadku – z VIM). W ramach swoich poszukiwań, znalazłem projekt Facebook PFFF. W treści tego wpisu pokażę co te narzędzie potrafi (bazując na kodzie Drupala 8) oraz jak za pomocą tego projektu modyfikować oraz analizować kod źródłowy bazujący na PHP.

Facebook PFFF – co to takiego?

Facebook PFFF to tak naprawdę bogate API służące analizie i refaktoryzacji API oraz kilka narzędzi bazujących na tym API. Pewną wadą rozwiązania jako biblioteki jest fakt, że napisane jest w języku funkcyjnym OCaml, co nie świadczy o słabości technicznej rozwiązania, ale nie jest to najpopularniejszy język programowania. Na razie więc API jest ograniczone tylko do programistów tego języka. Zapewne większość studentów i absolwentów kierunków pokrewnych z informatyką spotkało się z nim przynajmniej w ramach jednego kursu, więc nie jest to też aż taki problem. To co w moim mniemaniu jest zaletą tego projektu jest fakt, że nie działa on na poziomie kodu źródłowego, ale na strukturze AST (https://en.wikipedia.org/wiki/Abstract_syntax_tree), będącej efektem sparsowania kodu źródłowego. Oznacza to, że narzędzia analizują nie tyle tekst programu, ale jego strukturę. Problemy które zdarzają się przy np. naiwnym wyszukiwaniu fragmentów za pomocą wyrażeń regularnych nie mają tu miejsca - psujące wyniki białe znaki, komentarze inlinowe, niespójne zasady formatowania kodu stają się przeszłością.

Jak to zainstalować?

Niestety, narzędzia dystrybuowane są tylko w formie kodu źródłowego, więc trzeba ubrudzić sobie ręce samodzielną kompilacją kodu. Narzędzia zaimplementowane są za pomocą mniej popularnego języka funkcyjnego Ocaml, do tego w zależnościach jest również interpreter języka logicznego Prolog, co może brzmieć trochę strasznie. Na szczęście nie było to takie trudne, jak by mogło by się wydawać. Proces samodzielnej kompilacji w Ubuntu był bardzo prosty: 

sudo apt-get install git
sudo apt-get install ocaml
sudo apt-get install libcairo-ocaml-dev
sudo apt-get install swi-prolog

Następnie pobieramy kod z github

git clone https://github.com/facebook/pfff

następnie w katalogu projektu kompilujemy kod i instalujemy binarki w katalogu /usr/local/bin

./configure
make depend
make
make install

Na pewno nie jest to najwygodniejszy sposób instalacji, niemniej być może w przyszłości pojawi się dystrybucja w formie binarnej.

Co w zestawie narzędzi?

Tak jak napisałem poprzednio, PFFF, to zestaw narzędzi i API. Z zestawu narzędzi, które wydają się interesujące do moich zastosowań wybrałem: sgrep – czyli semantyczny grep, spatch – czyli semantyczny patch oraz codequery, czyli narzędzie do odpytywania kodu o jego strukturę, bazujący na języku logicznym prolog.. Jest jeszcze kilka narzędzi (pełna lista na stronie projektu), niemniej nie mają dla mnie dużego znaczenia, dlatego pominę je w tym poście.

SGREP – czyli semantyczne wyszukiwanie

Pewnie każdy mające minimalne doświadczenie z systemami UNIX-opodobnymi wie doskonale czym jest narzędzie grep. Tutaj mamy do czynienia z grepem na sterydach :). Te narzędzie pozwala wyszukiwać elementy AST (a więc abstrakcyjnej struktury programu, a nie tekstu kodu źródłowego).

Pokażmy, co potrafi te narzędzie:

Najpierw prosty przykład, chciałbym znaleźć wszystkie wywołania funkcji strpos, przy których wynik działania funkcji jest porównywany w sposób identycznościowy do wartości booleanowskiej FALSE

find|egrep "(*.php|*.inc|*.module)$"|xargs sgrep -e "strpos(...) !== FALSE"

other.d8/core/includes/bootstrap.inc:438: strpos($request->headers->get('Accept-Encoding'),'gzip')!==FALSE
other.d8/core/includes/common.inc:743: strpos($variables['options']['attributes']['title'],'<')!==FALSE
other.d8/core/includes/common.inc:1016: strpos($options['data'],'?')!==FALSE
other.d8/core/includes/install.core.inc:301: strpos($request->server->get('HTTP_USER_AGENT'),'simpletest')!==FALSE
other.d8/core/lib/Drupal/Component/Gettext/PoItem.php:193: strpos($this->_source,LOCALE_PLURAL_DELIMITER)!==FALSE
other.d8/core/lib/Drupal/Component/Utility/Tags.php:57: strpos($tag,',')!==FALSE
other.d8/core/lib/Drupal/Component/Utility/Tags.php:57: strpos($tag,'"')!==FALSE

 

Zauważcie, że użyłem tutaj wyrażenia (…). W sgrep oznacza ono, że łapię wszystkie wywołania tej funkcji, i nie przejmuję się, czy przekazywane są stałe, czy też zmienne PHP. Samo narzędzie karmione jest listą plików. Dlatego zastosowałem tutaj wyrażenie find|egrep "(*.php|*.inc|*.module)$", znajdujące wszystkie pliki o rozszerzeniu php, inc oraz module (bo badamy Drupala, w którym takie pliki też występują) w bieżącym katalogu. Spróbujmy inny przykład - wiadomo że w przypadku standardowego testu różnicy (!=) wyniki strpos mogą być mylące.

Sprawdźmy czy w kodzie nie znajdziemy tego potencjalnego błędu. 

find|egrep "(*.php|*.inc|*.module)$"|xargs sgrep = -e "strpos(...) != FALSE"

Tutaj brak wyników, czyli dobrze :)

Spróbujmy bardziej skomplikowanego przykładu. SGrep obsługuje metazmienne, a więc zmienne w wyrażeniu, za które będą podstawiane fragmentu kodu, które pasują akurat do wyrażenia. Metazmienne zapisywane są dużymi literami. Do tego sgrep pozwala wyświetlać wartości podstawionych metazmiennych (za pomocą przełącznika -pvar). Sprawdźmy jakie argumenty przekazywane są jakie pierwsze argumenty funkcji array_map. Intuicyjnie wiemy, że pierwszym argumentem jest callback który nakładany jest na wartości drugiej zmiennej

find|egrep "(*.php|*.inc|*.module)$"|xargs sgrep -pvar Z -e "array_map(Z, X)"
other.d8/core/lib/Drupal/Component/Gettext/PoHeader.php:259: 'trim'
other.d8/core/lib/Drupal/Core/Block/BlockBase.php:189: array(\Drupal::service('date.formatter'),'formatInterval')
other.d8/core/lib/Drupal/Core/Block/BlockManager.php:93: function function($definition){return$definition['category'];}
other.d8/core/lib/Drupal/Core/Cache/ApcuBackend.php:236: array($this,'getApcuKey')
other.d8/core/lib/Drupal/Core/Cache/DatabaseBackend.php:300: array($this,'normalizeCid')
other.d8/core/lib/Drupal/Core/Cache/DatabaseBackend.php:373: array($this,'normalizeCid')
other.d8/core/lib/Drupal/Core/Config/CachedStorage.php:303: use($prefix)function function($name){return$prefix.$name;}
other.d8/core/lib/Drupal/Core/Config/ConfigImporter.php:376: use($module_data)function function($module){return$module_data[$module]->sort;}
other.d8/core/lib/Drupal/Core/Config/Entity/Query/Condition.php:40: 'Drupal\Component\Utility\Unicode::strtolower'
other.d8/core/lib/Drupal/Core/Config/Entity/Query/Query.php:148: use($prefix)function function($id){return$prefix.$id;}
other.d8/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php:297: array($this,'resolveServices')

Jak widać dostałem listę callbacków które były przekazywane jako pierwszy argument do funkcji array_map. W liście wyników nie jest zachowany format kodu.

Poszukajmy jakiegoś kodu obiektowego, sprawdzmy gdzie rzucany jest wyjątek AccessException.

find|egrep "(*.php|*.inc|*.module)$"|xargs sgrep -oneline -e "throw new AccessException(...);"

other.d8/core/lib/Drupal/Core/Access/AccessManager.php:218: throw new AccessException("Access error in $service_id. Access services must return an object that implements AccessResultInterface."); 
other.d8/core/lib/Drupal/Core/Access/CheckProvider.php:105: throw new AccessException('All access checks must implement AccessInterface.'); 
other.d8/core/lib/Drupal/Core/Access/CheckProvider.php:108: throw new AccessException(sprintf('Access check method %s in service %s must be callable.',$this->checkMethods[$service_id],$service_id)); 

Chciałbym sprawdzić, w jakie argumenty przekazywane są przy rzucaniu tego wyjątku

find|egrep "(*.php|*.inc|*.module)$"|xargs sgrep -oneline -pvar Z -e "throw new AccessException(Z);"

other.d8/core/lib/Drupal/Core/Access/AccessManager.php:218: "Access error in $service_id. Access services must return an object that implements AccessResultInterface." 
other.d8/core/lib/Drupal/Core/Access/CheckProvider.php:105: 'All access checks must implement AccessInterface.' 
other.d8/core/lib/Drupal/Core/Access/CheckProvider.php:108: sprintf('Access check method %s in service %s must be callable.',$this->checkMethods[$service_id],$service_id) 

W Drupalu 8 zaimplementowany jest wzorzec Dependency Injection Container. Aby skorzystać z kontenera, należy użyć statycznej metody Drupal::service. Sprawdźmy, gdzie i jakie usługi wykorzystywane są w kodzie aplikacji:

find|egrep "(*.php|*.inc|*.module)$"|xargs sgrep -oneline -pvar Z -e "\Drupal::service(Z)"

other.d8/core/authorize.php:54: 'session_manager'
other.d8/core/authorize.php:157: 'bare_html_page_renderer'
other.d8/core/includes/batch.inc:44: 'batch.storage'
other.d8/core/includes/batch.inc:138: 'bare_html_page_renderer'
other.d8/core/includes/batch.inc:319: 'date.formatter'
other.d8/core/includes/batch.inc:321: 'date.formatter'
other.d8/core/includes/batch.inc:411: 'date.formatter'
other.d8/core/includes/batch.inc:418: 'batch.storage'

Mam nadzieję, że te przykłady pokazują, że używanie tego narzędzia nie jest potwornie skomplikowane. Faktem jest, że można te informacje wyciągnąć za pomocą wyrażeń regularnych, a le na pewno kosztem przejrzystości. Niestety, przed twórcami jest jeszcze trochę pracy – na liście rzeczy do zrobienia jest wsparcie dla instrukcji (a nie wyrażeń), czyli np. obsługi pętli, operacji warunkowych i temu podobnych. Mimo tego, jak widać, samo narzędzie pozwala całkiem fajnie wyszukiwać interesujące nas fragmenty kodu, bez martwienia się o problemy związane z wyrażeniami regularnymi, takimi jak różne białe znaki, konwencje formatowania czy inne.

Więcej informacji o narzędziu można znaleźć tutaj:

https://github.com/facebook/pfff/wiki/Sgrep

SPATCH – czyli patch na sterydach

Tak jak pisałem we wstępie, potrzebuję narzędzia do przeprowadzania refaktoryzacji (czyli modyfikacji strutktur klas, nagłówków funkcji i innych fragmentów kodu). Jak zawsze, można próbować zasegurować zwykłe tekstowe podstawienie, ale z doświadczenia wiem, że takie podejście często nie działa dobrze, często z jakiś powodów, jakieś wywołanie funkcji, metody pozoastanie niezmienione. Jeżeli mamy dobry zestaw kodów jednostkowych, problem wychwycimy szybko, jeśli nie – cóż – nie każdy lubi wstawać o północy by debugować problemy na produkcji.

Pokażmy kilka przykładów (jako kod bazowy, używam Drupal-a 8):

W kodzie można znaleźć trochę wywołań exit. Można uznać to za złą praktykę, i chcieć obsłużyć to w jakiś inny sposób. Np. Chciałbym zastąpić wywołanie exit rzuceniem wyjątki ServeExit (oczywiście wyjątek ten nie istnieje).

Przypadek drugi: Załóżmy że chciałbym zamienić wykorzystywane w niektórych wywołaniach array_map callback „trim” na nowy lepszy: foo_trim

find|egrep "(*.php|*.inc|*.module)$"|xargs spatch -e "s/array_map('trim', X)/array_map('foo_trim', X)/g"

--- other.d8/core/lib/Drupal/Component/Gettext/PoHeader.php	2014-10-03 09:08:49.669193658 +0200 
+++ /tmp/trans-21286-40da22.spatch	2014-12-25 13:02:47.806016270 +0100 
@@ -256,7 +256,7 @@ 
    */ 
   private function parseHeader($header) { 
     $header_parsed = array(); 
-    $lines = array_map('trim', explode("\n", $header)); 
+    $lines = array_map('foo_trim', explode("\n",$header)); 
     foreach ($lines as $line) { 
       if ($line) { 
         list($tag, $contents) = explode(":", $line, 2); 
--- /home/jsobiecki/workspace/codebase/other.d8/core/lib/Drupal/Core/EventSubscriber/ModuleRouteSubscriber.php	2014-10-03 09:08:49.693193659 +0200 
+++ /tmp/trans-21286-25311d.spatch	2014-12-25 13:02:51.870016120 +0100 
@@ -78,7 +78,7 @@ 
    *   An array of exploded (and trimmed) values. 
    */ 
   protected function explodeString($string, $separator = ',') { 
-    return array_filter(array_map('trim', explode($separator, $string))); 
+    return array_filter(array_map('foo_trim', explode($separator,$string))); 
   } 
 
 } 
-----------------------------CUT-----------------------------------------------.

Kolejny przykład, dodajmy do wywołań funkcji node_last_changed opcjonalny argument. Funkcja node_last_changed, pobiera dwa argumenty, z których jeden jest opcjonalny. Znajdzmy więc miejsca w kodzie, gdzie drugi argument nie był podany, i zastąpmy go jakimś argumentem, w naszym przypadku 'PL'.

find|egrep "(*.php|*.inc|*.module)$"|xargs spatch -e "s/node_last_changed(X)/node_last_changed(X, 'PL')/g"
--- other.d8/core/modules/node/src/Tests/NodeLastChangedTest.php	2014-12-20 16:41:16.769096412 +0100 
+++ /tmp/trans-21464-0615cc.spatch	2014-12-25 13:04:29.742012484 +0100 
@@ -37,7 +37,7 @@ 
     $node->save(); 
 
     // Test node last changed timestamp. 
-    $changed_timestamp = node_last_changed($node->id()); 
+    $changed_timestamp = node_last_changed($node->id(), 'PL'); 
     $this->assertEqual($changed_timestamp, $node->getChangedTime(), 'Expected last changed timestamp returned.'); 
 
     $changed_timestamp = node_last_changed($node->id(), $node->language()->getId()); 

W obu tych przykładach, polecenie wygeneruje patch, który można aplikować w celu modyfikacji kodu (np. za pomocą starego dobrego patch). Ewentualnie (dla odważnych) można użyć przełącznika –apply-patch, by od razu modyfikować kod.

Niestety, spatch nie spełnia jeszcze wszystkich moich potrzeb. Wg. Dokumentacji API jest kompletne, niemniej samo narzędzia spatch nie implementuje jeszcze wszystkich jej funkcjonalności. Dla przykładu nie można zmieniać nagłówków metod czy funkcji, czy np. zmieniać definicji klas. Pozostaje mieć nadzieję, że twórcy zajmą się tymi problemami i wkrótce narzędzie będzie kompletnym kombajnem do refaktoryzacji kodu z linii poleceń.

 

CODEQUERY – czyli zaawansowana analiza struktury

Kolejnym narzędziem z pakietu jest codequery. To narzędzie, które buduje bazę danych na podstawie struktury kodu, a następnie pozwala wykonywać zapytania. Jest to narzędzie (przynajmniej na bazie moich testów) dużo potężniejsze niż sgrep, ale także, co za tym idzie bardziej skomplikowane.

 

Można o tym pomyśleć, jako o cscope na sterydach, chociaż analogia nie jest do końca poprawna. Twórcy projektu PFFF, zaimplementowali powiem narzędzie które pozwala odpytywać strukturę kodu projektu, za pomocą wyrażeń języka prolog. Brzmi być może strasznie, na szczęście by osiągnąć ciekawe wyniki nie trzeba być mistrzem programowania logicznego. Pokażmy na przykładzie jak to działa – najpierw musimy wygenerować bazę wiedzy, a więc pozwolić narzędziu ustalić wszystkie fakty dotyczące struktury naszego kodu

codequery -build .

Polecenie to wygeneruje plik facts.pl – jest to plik tekstowy, stanowiązy bazę wiedzy – listę faktów o naszym kodzie. Znajdziemy tutaj min. informacje co jest klasą, co jest funkcją, jaka metoda wywoływana jest gdzie itd. W zasadzie jest to wszystko co potrzebujemy wiedzieć.

Następnie za pomocą polecenia

swipl -s facts.pl

uruchamiamy interpreter prologa i ładujemy bazę faktów. Teraz pozostaje tylko zadać odpowiednie zapytanie do systemu, by uzyskać dobrą odpowiedź. To najtrudniejsza część :)

Prolog to język deklaratywny a więc taki, w którym nie tyle opisujemy maszynie proces który docelowa ma osiągnąć wymagany wynik a samą logikę która ten wynik opisuje. Pierwotnie ten język był stosowany przy zastosowaniach sztucznej inteligencji, można go również stosować do implementacji baz danych (Dla ciekawych: https://en.wikipedia.org/wiki/Datalog).

Prolog określany jest językiem logicznym, a więc takim, w którym program piszemy, używając wyrażeń logicznych – w wyrażeniach używamy faktów (np. to_jest_pies('Atos').) oraz reguł (czyli np. jest(swiatlo) :- wlaczony(przycisk)). W przypadku naszych zastosowan, wystarczy laczyc fakty za pomocą operatorow logicznych (takich jak „i”) by osiągnąć ciekawe wyniki.

W prologu, podobnie jak wcześniej opisanych narzędziach sgrep czy spatch można używać zmiennych – prolog jest w stanie dopasować zmienne do wyrażenia tak by osiągnąć wyrażenie które jest prawdziwe. Zdaje sobie sprawę, że może to być mętne, spróbujmy więc od przykładu. Zapytajmy się prologa, jakie znalazł w kodzie stałe:

kind(X, constant).
X = 'MAINTENANCE_MODE' ;
X = 'DRUPAL_MINIMUM_PHP' ;
X = 'DRUPAL_MINIMUM_PHP_MEMORY_LIMIT' ;
X = 'ERROR_REPORTING_HIDE' ;
X = 'ERROR_REPORTING_DISPLAY_SOME' ;
X = 'ERROR_REPORTING_DISPLAY_ALL' ;
X = 'ERROR_REPORTING_DISPLAY_VERBOSE' ;
X = 'DRUPAL_BOOTSTRAP_CONFIGURATION' ;
X = 'DRUPAL_BOOTSTRAP_KERNEL' ;
X = 'DRUPAL_BOOTSTRAP_PAGE_CACHE' ;
X = 'DRUPAL_BOOTSTRAP_CODE' ;
------------------CUT--------------------

Prolog wyświetli nam listę wszystkich stałych znalezionych w kodzie. Za wyrażenie X podstawione zostaną te wartości, dla których wyrażenie kind(X, constant) jest prawdziwe, np. MAINTENANCE_MODE.

Mam nadzieję że tłumaczy to ideę. Ok, a jakie traits mamy zdefinowane w kodzie Drupala?

kind(X, trait).
X = 'DiscoveryCachedTrait' ; 
X = 'DiscoveryTrait' ; 
X = 'ConditionAccessResolverTrait' ; 
X = 'ThirdPartySettingsTrait' ; 
X = 'SchemaCheckTrait' ; 
X = 'DependencySerializationTrait' ; 
X = 'DependencyTrait' ; 
X = 'EntityTypeEventSubscriberTrait' ; 
X = 'AllowedTagsXssTrait' ; 
X = 'FieldStorageDefinitionEventSubscriberTrait' ; 
-----------------------CUT------------------------

To nic trudnego, spróbujmy ustalić coś ciekawszego – jakie bloki (w Drupalu 8 to klasy implementujące klasę BlockBase) dostępne są w kodzie oraz w jakich plikach znajdziemy ich implementacje. Tutaj użyję operatora logicznego „i” - w składni prologa to przecinek, oraz zmiennej _ - której wartość nas nie obchodzi.

extends(X, 'BlockBase'), at(X, Z, _).
Z = 'core/modules/system/src/Plugin/Derivative/SystemMenuBlock.php' ; 
X = 'SystemPoweredByBlock', 

Z = 'core/modules/system/src/Plugin/Block/SystemPoweredByBlock.php' ; 
X = 'RedirectFormBlock', 

Z = 'core/modules/system/tests/modules/form_test/src/Plugin/Block/RedirectFormBlock.php' ; 
X = 'UserLoginBlock', 

Z = 'core/modules/user/src/Plugin/Block/UserLoginBlock.php' ; 
X = 'ViewsBlockBase', 
-------------------CUT-----------------------------------------------------

Użyłem faktu extends, który okręśla relację dziedziczenia

Ok, na liście wyników pojawiła się klasa SystemPoweredByBlock, sprawdzmy, jakie metody publiczne deklaruje ta klasa:

kind(('SystemPoweredByBlock', X), method),is_public(('SystemPoweredByBlock', X)).
X = build ; 
X = buildConfigurationForm ; 
X = getCacheMaxAge ; 
X = isCacheable ; 

Ok, na liście mamy metodę buildConfigurationForm(). Chciałbym sprawdzić jakie funkcje (nie mylić z metodami!) wywołuje ta metoda.

docall(('SystemPoweredByBlock', 'buildConfigurationForm'), X, 'function' ).
X = t ;

Lista faktów które analizuje narzędzie codequery nie jest bardzo rozbudowana i łatwo można je poznać przez samodzielną analizę pliku facts.pl. Widzę duży potencjał w samym narzędziu, ale trochę mi brakowało – np. codequery nie zbiera informacji o zmiennych używanych w kodzie funkcji czy metod (co jest trudne, ze względu na dynamiczny system typów użyty w PHP). Niemniej, wydaje mi się, że porównując to narzędzie np. z cscope – widzę tu dużo większą elastyczność samego narzędzia – odpytywanie się o strutkurę kodu za pomocą wyrażeń logicznych wydaje się być niesamowicie elastyczne, łatwo np.  szybko ustalić hierarchię klas, lub elementy interfejsu który potrzebny jest do zaimplementowania.

Plany na przyszłość

Niestety, narzędzia które opisałem nie są idealne – brakuje kilku funkcjonalności, np. w przypadku spatch – możliwości modyfikacji struktur, oraz instrukcji, a nie tylko fragmentów wyrażeń. Brakuje też jakiejś integracji ze środowiskami IDE, czy też w moim wypadku VIM. Niemniej, same narzędzia wydają się bardzo obiecujące i stoi za nimi duży potencjał. Gdy znajdę trochę czasu, postaram się przygotować wtyczkę dla VIM, która będzie integrowała opisane narzędzia z tym edytorem.