Dla Kotlina i Javy jednocześnie
TL;DR
W tej pierwszej części artykułu chcę przede wszystkim pokazać, że warto poświęcić czas na napisanie własnej reguły analizy kodu, aby wymusić konwencje kodu. Po drugie, chcę pokazać, że oprócz Checkstyle i ktlint, Android Lint powinien być również brany pod uwagę przy tworzeniu reguły analizy kodu związanej z formatowaniem, mimo że nie jest to jego podstawowe przeznaczenie. Wyjaśnię jego główną zaletę w moim przypadku w porównaniu z innymi narzędziami i opiszę wewnętrzne działanie, które zapewnia tę korzyść. Dla inspiracji, pokażę również kroki, które podjąłem, aby moja reguła formatowania działała z Android Lint.
Motywacja
Byłem sfrustrowany, kiedy dostawałem komentarze związane z formatowaniem do moich wniosków o scalenie lub kiedy musiałem dodawać takie komentarze do wniosków innych. Czasami musimy zmieniać gałęzie, aby naprawić pojedynczy błąd formatowania w naszym MR, co jest dość demotywujące. Dlatego też zacząłem się zastanawiać, w jaki sposób moglibyśmy zautomatyzować naprawianie tego drobnego (ale ważnego) problemu, abyśmy mogli skupić się na bardziej złożonych kwestiach podczas przeglądów kodu.
Używamy kilku narzędzi typu lint, takich jak ktlint czy Checkstyle, do wymuszania globalnych konwencji formatowania (kolejność zmiennych, długość linii, itp.), ale nie zawierają one domyślnie pewnych reguł obowiązujących w całej firmie. Dla takich przypadków, wyżej wymienione narzędzia wspierają dodawanie własnych reguł. Postanowiłem napisać jedną dla najczęstszych błędów formatowania, które popełniamy:
Zawsze wymagamy podwójnych przerw w linii przed i po „block statements”, którymi mogą być bloki if, switch (when w Kotlinie), for, try, lub while, chyba że znajdują się dokładnie na początku lub końcu metody lub innego bloku. Wierzymy, że to czyni nasz kod bardziej czytelnym, dlatego zawsze zostawiamy komentarz podczas code review, jeśli ktoś narusza tę zasadę (i jeśli zdarzy nam się to zauważyć).
Jakie jest najlepsze narzędzie do tego problemu
Możemy powiedzieć, że ktlint dla Kotlina i Checkstyle dla Javy są głównymi narzędziami do analizy kodu formatującego, ale teraz wybiorę jeszcze inne narzędzie: Android Lint (nie tylko dla projektów Android), ponieważ obsługuje również pisanie niestandardowych reguł, które można zastosować do Kotlina i Javy w tym samym czasie. Wydaje się to bardziej logicznym wyborem, ponieważ używamy obu języków w naszych projektach na Androida, a ja nie chciałem pisać tej samej reguły dwa razy ani utrzymywać ich współbieżnie. Nie wspominając już o integracji reguły, która również musiałaby być wykonana dwa razy.
Dlaczego Java jest w ogóle jeszcze ważna
Jako programista Androida muszę pisać kod zarówno w Javie jak i Kotlinie. Można jednak powiedzieć, że nie warto już skupiać się na Javie, ponieważ wraz z pojawieniem się Kotlina, coraz częściej używamy go zamiast Javy w naszej bazie kodowej. Z drugiej strony uważam, że w dużych projektach, które są już w produkcji, przejście nie następuje tak szybko. Tak więc, ponieważ chcemy widzieć ładnie sformatowany kod również w Javie, ważne jest, aby mieć tę regułę dla obu języków.
Jak napisać własne reguły lint
Istnieje wiele tutoriali i artykułów na temat pisania własnych reguł analizy kodu, więc nie będę tego zbytnio opisywał w tym poście. Ale jeśli jesteś zainteresowany, oto kilka linków, które mogę polecić dla różnych narzędzi: Do Checkstyle, oficjalny doc, do ktlint i Android Lint, świetne artykuły medium Niklasa Baudy’ego.
Jak działa Android Lint i inne narzędzia do statycznej analizy kodu
Początkowo Android Lint został stworzony do znajdowania głównie specyficznych dla Androida błędów w kodzie Java. W celu przeanalizowania kodu Java, Android Lint użył do stworzenia specyficznego dla Javy Abstrakcyjnego Drzewa Składni (lub po prostu AST, dowiedz się więcej na Wikipedii), które jest drzewiastą reprezentacją kodu źródłowego. Inne narzędzia do statycznej analizy kodu również używają specyficznego dla języka AST do analizy;
Checkstyle: Java-, ktlint: Kotlin-, detekt: Kotlin-specific. Narzędzia przemierzają ten AST i znajdują błędy poprzez sprawdzanie jego węzłów i ich atrybutów.
Jak Android Lint może sprawdzać dwa języki jednocześnie
Różnica pomiędzy Android Lint a innymi narzędziami jest taka, że gdy Kotlin stał się językiem wspieranym przez Androida, Android Lint zaczął wspierać Kotlin również w odniesieniu do zasad, które już mieliśmy dla Javy. W tym celu wprowadzono tak zwane uniwersalne abstrakcyjne drzewo składniowe (opracowane przez JetBrains), które zapewnia drzewiastą reprezentację kodu, która może być stosowana zarówno dla języków Kotlin, jak i Java w tym samym czasie, więc jest na wyższym poziomie abstrakcji niż specyficzny dla języka AST. Aby uzyskać więcej informacji, polecam tę część wykładu Tor Norbye – twórcy i opiekuna Android Lint.
Oba UAST i AST dostarczają szczegółów wysokiego poziomu na temat kodu źródłowego. Nie zawierają one informacji o białych spacjach czy nawiasach klamrowych, ale to zazwyczaj wystarcza dla reguł specyficznych dla Androida. Zobacz przykład UAST poniżej:
Biblioteka UAST zawiera wszystkie wyrażenia specyficzne dla danego języka, ale również udostępnia dla nich wspólne interfejsy. Przykład dla wyrażenia if:
Drzewo PSI (Program Structure Interface Tree)
Drzewo PSI jest zbudowane na UAST w przypadku Android Lint (w przypadku innych narzędzi, jest zbudowane na specyficznym dla języka AST), i jest to drzewo, które zawiera więcej szczegółów o strukturze kodu, takich jak spacje, nawiasy klamrowe i podobne elementy.
W Android Lint, wyrażenia PSI są zawarte w implementacji wyrażeń UAST, które są węzłami UAST. Np. org.jetbrains.kotlin.psi.KtIfExpression może być dostępne z KotlinUIfExpression.
Istnieje nawet poręczna wtyczka intelliJ: PsiViewer, który może ułatwić debugowanie reguł analizy kodu opartych na drzewie PSI. Zobacz poniższy przykład z tym samym fragmentem kodu, gdzie możemy zobaczyć, że dwa języki mają różne tokeny specyficzne dla danego języka w drzewach:
Używanie zarówno UAST, jak i PSI Tree
Potrzebuję zarówno UAST, jak i PSI Tree, aby stworzyć moją regułę formatowania, ponieważ UAST jest świetny do filtrowania i odwiedzania węzłów, które mnie interesują dla obu języków naraz, ale jak wspomniałem nie dostarcza informacji o formatowaniu, np. informacji o białych odstępach, które są dla mnie kluczowe. UAST skupia się raczej na wyższym poziomie abstrakcji, więc przede wszystkim nie służy do tworzenia reguł formatowania.
Od czasu wtyczki Gradle 3.4 i związanego z nią Android Lint w wersji 26.4.0, możemy uzyskać drzewiastą reprezentację PSI każdego węzła i jego otoczenia, nie tylko w Javie, ale również w Kotlinie. To sprawia, że możliwe jest użycie Android Lint dla mojej reguły formatowania.
Jak zaimplementowana jest reguła
Po pierwsze, muszę utworzyć mój problem, gdzie ustawiam zakres na JAVA_FILE, co oznacza zarówno Javę jak i Kotlin w Android Lint obecnie. (W tym miejscu możemy ustawić również inne rodzaje plików, takie jak XML czy pliki Gradle, ponieważ Android Lint może je również sprawdzać.)
Wtedy, w mojej klasie detektora, Wyliczam węzły, którymi jestem zainteresowany wewnątrz funkcji getApplicableUastTypes, więc Android Lint będzie wiedział, że powinien wywołać powiązane nadpisane metody odwiedzin, takie jak visitForEachExpression. W każdej metodzie wizyty, po prostu wywołuję moją metodę sprawdzającą.
W mojej metodzie sprawdzającej, parametr forward opisuje, czy chcę sprawdzić przerwanie linii przed lub po „oświadczeniu blokowym”. W ciele metody, badam liczbę białych spacji wokół odwiedzanego przeze mnie oświadczenia blokowego. W tym celu wykonuję trzy główne kroki:
- Pierwszy, przez metodę firstBlockLevelNode, sprawdzam jaki jest pierwszy węzeł poziomu bloku, wokół którego chcę sprawdzić whitespaces, ponieważ jeśli używam bloku if do przypisania wartości zmiennej, jak np,
Lint zbadałby whitespace tuż przed słowem kluczowym if, ale mnie interesuje whitespace przed słowem kluczowym val. Tak więc, w tym przypadku, pierwszą deklaracją na poziomie bloku jest przypisanie wartości, które owija moją instrukcję if.
- Po drugie, w metodzie firstRelevantWhiteSpaceNode, sprawdzam jaki jest pierwszy odpowiedni węzeł whitespace, gdzie powinniśmy policzyć podziały linii. Czasami nie ma żadnego istotnego whitespace do sprawdzenia, ponieważ jeśli mój blok jest na początku lub końcu metody lub innych bloków, to jest w porządku i mogę zrezygnować z dalszego dochodzenia. Zobacz:
W tym momencie używam już węzłów PSI, ponieważ chcę sprawdzić informacje o whitespace, które nie są dostarczane w UAST. W tym celu muszę uzyskać właściwość sourcePsi węzła UAST na poziomie bloku.
Przypadek brzegowy to sytuacja, gdy komentarz znajduje się tuż nad moją deklaracją blokową. W tym przypadku chcę sprawdzić przestrzeń białą nad komentarzem, więc pierwsza odpowiednia przestrzeń biała znajduje się nad komentarzem.
- W końcu liczę liczbę przerw w linii w odpowiedniej przestrzeni białej; Jeśli nie jest ona większa niż 1, zgłaszam problem.
Wynik
W wyniku oczekuję następujących ostrzeżeń w IDE, więc dla każdego programisty będzie jasne, że należy dodać dodatkową linię. Mogę również uzyskać te ostrzeżenia w raporcie Lint, jeśli uruchomię zadanie lint gradle. Co więcej, mogę nawet podnieść dotkliwość problemu do błędu, jeśli chcę zablokować zadanie żądania scalenia.
Wniosek
Po zintegrowaniu reguły niestandardowej, nie musimy się już skupiać na tym frustrującym problemie; Zamiast tego, możemy poświęcić naszą energię umysłową na znalezienie bardziej złożonych problemów podczas wzajemnego przeglądania kodu. Jeszcze lepiej, możemy w 100% zagwarantować, że ten błąd nie dostanie się przypadkowo do naszej bazy kodu, ponieważ możemy skonfigurować nasze zadanie MR tak, aby nie powiodło się, gdy ktoś nie zdąży naprawić tego problemu.
Chociaż Android Lint nie służy głównie do formatowania reguł, w moim przypadku był całkiem praktyczny, ponieważ można go używać zarówno dla Javy, jak i Kotlina: nie trzeba pisać podwójnych reguł, utrzymywać ich i integrować.
Z drugiej strony, musimy zauważyć, że ta reguła jest bardzo prosta w tym, że muszę tylko sprawdzić białe znaki i nawiasy klamrowe na poziomie PSI, które są takie same w obu językach. Dlatego też nie muszę pisać żadnego kodu specyficznego dla danego języka. Jednakże, gdybym musiał napisać jakiś specyficzny dla języka kod (np. obsługa operatora Elvis w Kotlinie lub operatora Ternary w Javie), to i tak wolałbym Androida Lint od pisania one-one ktlint i reguł Checkstyle, ponieważ miałbym prawdopodobnie mniej pracy.
Następna część…
Jeśli podobała Ci się pierwsza część artykułu, proszę sprawdź również drugą, w której szczegółowo omówię integrację mojej reguły w projektach Androidowych (i nie-Androidowych), jak naprawić już istniejące problemy w naszej bazie kodu, oraz jak możemy testować jednostkowo i debugować nasze reguły.