Formatierung der Code-Analyse-Regel mit Android Lint (Teil 1/2)

Für Kotlin und Java auf einmal

Balázs Ruda
Balázs Ruda

Follow

9. Sep, 2019 – 9 min read

TL;DR

In diesem ersten Teil des Artikels möchte ich in erster Linie zeigen, dass es die Zeit wert ist, eine eigene Regel für die Codeanalyse zu schreiben, um Codekonventionen durchzusetzen. Zweitens möchte ich zeigen, dass man neben Checkstyle und ktlint auch Android Lint in Betracht ziehen sollte, wenn man eine formatierungsbezogene Code-Analyseregel erstellt, obwohl das nicht sein Hauptzweck ist. Ich werde seinen Hauptvorteil in meinem Fall gegenüber den anderen Tools erläutern und die inneren Abläufe beschreiben, die diesen Vorteil bieten. Zur Inspiration zeige ich auch die Schritte, die ich unternommen habe, um meine Formatierungsregel mit Android Lint zum Laufen zu bringen.

Motivation

Ich bin frustriert, wenn ich formatierungsbezogene Kommentare zu meinen Merge-Anfragen bekomme oder wenn ich solche Kommentare zu denen anderer hinzufügen muss. Manchmal müssen wir Zweige wechseln, um einen einzigen Formatierungsfehler in unserer MR zu beheben, was ziemlich demotivierend ist. Deshalb habe ich angefangen, darüber nachzudenken, wie wir die Behebung dieses kleinen (aber wichtigen) Problems automatisieren können, damit wir uns während der Code-Reviews auf komplexere Probleme konzentrieren können.

Wir benutzen verschiedene Lint-Tools, wie ktlint oder Checkstyle, um globale Formatierungskonventionen (Variablenreihenfolge, Zeilenlänge, etc.) durchzusetzen, aber diese beinhalten standardmäßig bestimmte unternehmensweite Regeln nicht. Für diese Fälle unterstützen die oben genannten Werkzeuge auch das Hinzufügen von benutzerdefinierten Regeln. Ich habe mich entschieden, eine für die häufigsten Formatierungsfehler zu schreiben, die wir machen:

Fehlende Leerzeile um „Blockanweisungen“

Wir verlangen immer doppelte Zeilenumbrüche vor und nach „Blockanweisungen“, die if-, switch- (wenn in Kotlin), for-, try- oder while-Blöcke sein können, es sei denn, sie stehen genau am Anfang oder am Ende einer Methode oder eines anderen Blocks. Wir glauben, dass dies unseren Code lesbarer macht, deshalb hinterlassen wir beim Code-Review immer einen Kommentar, wenn jemand gegen diese Regel verstößt (und wenn wir es zufällig bemerken).

Was ist das beste Tool für dieses Problem

Wir können sagen, dass ktlint für Kotlin und Checkstyle für Java die wichtigsten Tools für die Code-Analyse sind, aber jetzt wähle ich noch ein anderes Tool: Android Lint (nicht nur für Android-Projekte), weil es auch das Schreiben eigener Regeln unterstützt, die gleichzeitig auf Kotlin und Java angewendet werden können. Es scheint mir eine logischere Wahl zu sein, da wir beide Sprachen in unseren Android-Projekten verwenden und ich weder die gleiche Regel zweimal schreiben noch sie gleichzeitig pflegen wollte. Ganz zu schweigen von der Integration der Regel, die ebenfalls zweimal gemacht werden müsste.

Warum Java überhaupt noch wichtig ist

Als Android-Entwickler muss ich Code sowohl in Java als auch in Kotlin schreiben. Allerdings kann man sagen, dass es sich nicht mehr lohnt, sich auf Java zu konzentrieren, denn mit dem Aufkommen von Kotlin verwenden wir es mehr und mehr anstelle von Java in unserer Codebase. Auf der anderen Seite glaube ich, dass der Übergang in großen Projekten, die bereits in Produktion sind, nicht so schnell passiert. Da wir also auch in Java schön formatierten Code sehen wollen, ist es wichtig, diese Regel für beide Sprachen zu haben.

Wie man benutzerdefinierte Lint-Regeln schreibt

Es gibt viele Tutorials und Artikel über das Schreiben von benutzerdefinierten Code-Analyse-Regeln, daher werde ich in diesem Beitrag nicht zu sehr ins Detail gehen. Aber wenn Sie interessiert sind, hier sind ein paar Links, die ich für die verschiedenen Werkzeuge empfehlen kann: Zu Checkstyle, der offiziellen Doku, zu ktlint und Android Lint, Niklas Baudys großartigen Medium-Artikeln.

Beispiel für AST (Quelle: Wikipedia über Abstract Syntax Tree)

Wie Android Lint und andere statische Code-Analyse-Tools funktionieren

Initial wurde Android Lint dafür geschaffen, hauptsächlich Android-spezifische Probleme in Java-Code zu finden. Um den Java-Code zu analysieren, hat Android Lint einen Java-spezifischen Abstract Syntax Tree (oder einfach AST, mehr dazu auf Wikipedia) erstellt, der eine Baumdarstellung des Quellcodes ist. Andere statische Code-Analyse-Tools verwenden ebenfalls einen sprachspezifischen AST für die Analyse;
Checkstyle: Java-, ktlint: Kotlin-, detekt: Kotlin-spezifisch. Die Tools durchlaufen diesen AST und finden Fehler, indem sie dessen Knoten und deren Attribute überprüfen.

Wie Android Lint zwei Sprachen gleichzeitig überprüfen kann

Der Unterschied zwischen Android Lint und anderen Tools besteht darin, dass, als Kotlin ebenfalls eine unterstützte Sprache für Android wurde, Android Lint begann, auch Kotlin hinsichtlich der Regeln zu unterstützen, die wir bereits für Java hatten. Dazu wurde der sogenannte Universal Abstract Syntax Tree (entwickelt von JetBrains) eingeführt, der eine Baumrepräsentation des Codes bereitstellt, die sowohl für Kotlin als auch für Java gleichzeitig angewendet werden kann, also auf einer höheren Abstraktionsebene liegt als ein sprachspezifischer AST. Für weitere Informationen empfehle ich diesen Vortragsteil von Tor Norbye – dem Schöpfer und Maintainer von Android Lint.

Beide, UAST und AST, liefern High-Level-Details über den Quellcode. Sie enthalten keine Informationen über Whitespaces oder geschweifte Klammern, aber das reicht in der Regel für die Android-spezifischen Regeln aus. Sehen Sie sich unten ein UAST-Beispiel an:

Durch den Aufruf der asRecursiveLogString()-Methode auf einen bestimmten UAST-Knoten in der Android Lint API, können wir die UAST ausdrucken.

Die UAST-Bibliothek enthält alle sprachspezifischen Ausdrücke, bietet aber auch allgemeine Schnittstellen für diese. Beispiel für den if-Ausdruck:

Die gemeinsame Schnittstelle UIfExpression wird durch die JavaUIfExpression implementiert, JavaUTernaryExpression, und KotlinUIfExpression.

PSI-Baum (Program Structure Interface Tree)

Der PSI-Baum baut im Fall von Android Lint auf dem UAST auf (bei anderen Tools auf dem sprachspezifischen AST), und das ist der Baum, der mehr Details über die Struktur des Codes enthält, wie z.B. Whitespaces, Klammern und ähnliche Elemente.

In Android Lint sind die PSI-Ausdrücke in der Implementierung der UAST-Ausdrücke enthalten, die die Knoten des UAST sind. Z.B. org.jetbrains.kotlin.psi.KtIfExpression kann über KotlinUIfExpression aufgerufen werden.

Es gibt sogar ein praktisches intelliJ Plugin: PsiViewer, das das Debuggen von PSI-Baum-basierten Code-Analyseregeln erleichtern kann. Im folgenden Beispiel mit dem gleichen Codeschnipsel können wir sehen, dass die beiden Sprachen unterschiedliche sprachspezifische Token in den Bäumen haben:

PSI-Baumdarstellung einer Methode in Java und Kotlin

Verwendung von UAST und PSI Tree

Ich brauche sowohl UAST als auch PSI Tree, um meine Formatierungsregel zu erstellen, denn UAST ist großartig, um zu filtern und Knoten, die mich interessieren, für beide Sprachen gleichzeitig zu besuchen, aber wie ich bereits erwähnt habe, liefert es keine Informationen über die Formatierung, z.B. Whitespace-Informationen, die für mich essentiell sind. UAST konzentriert sich eher auf eine höhere Abstraktionsebene, ist also primär nicht zum Erstellen von Formatierungsregeln geeignet.

Seit dem Gradle-Plugin 3.4 und der zugehörigen Android-Lint-Version 26.4.0 können wir die PSI-Baumrepräsentation jedes Knotens und seiner Umgebung erhalten, nicht nur in Java, sondern auch in Kotlin. Das macht es möglich, Android Lint für meine Formatierungsregel zu verwenden.

Wie die Regel implementiert wird

Zunächst muss ich mein Issue erstellen, wobei ich den Scope auf JAVA_FILE setze, was in Android Lint derzeit sowohl Java als auch Kotlin bedeutet. (Hier können wir auch andere Arten von Dateien wie XML- oder Gradle-Dateien einstellen, da Android Lint auch diese prüfen kann.)

Initialisierung einer benutzerdefinierten Issue-Klasse

Dann, in meiner Detektor-Klasse, Ich zähle die Knoten, an denen ich interessiert bin, in der Funktion getApplicableUastTypes auf, damit Android Lint weiß, dass es die zugehörigen überschriebenen Besuchsmethoden wie visitForEachExpression aufrufen soll. In jeder visit-Methode rufe ich einfach meine Checker-Methode auf.

Wie man Android Lint mitteilt, welche Knoten wir prüfen wollen

In meiner Checker-Methode beschreibt der Forward-Parameter, ob ich den Zeilenumbruch vor oder nach der „Block-Anweisung“ prüfen will. Im Methodenrumpf untersuche ich die Zeilenumbruch-Anzahl der Whitespaces um die „Blockanweisung“, die ich besuche. Dazu führe ich im Wesentlichen drei Schritte aus:

  • Erst prüfe ich mit der Methode firstBlockLevelNode, welches der erste Block-Level-Knoten ist, um den herum ich die Whitespaces prüfen möchte, denn wenn ich einen if-Block für die Zuweisung eines Wertes an eine Variable verwende, wie hier,
„Blockanweisung“ bei der Wertzuweisung

Lint würde den Whitespace direkt vor dem if-Schlüsselwort untersuchen, aber ich bin an dem Whitespace vor dem val-Schlüsselwort interessiert. In diesem Fall ist also die erste Anweisung auf Blockebene die Wertzuweisung, die meine if-Anweisung umschließt.

  • Zweitens prüfe ich in der Methode firstRelevantWhiteSpaceNode, was der erste relevante Whitespace-Knoten ist, an dem wir die Zeilenumbrüche zählen sollten. Manchmal gibt es keinen relevanten Whitespace zu prüfen, denn wenn mein Block am Anfang oder Ende einer Methode oder eines anderen Blocks steht, dann ist das in Ordnung und ich kann auf weitere Untersuchungen verzichten. Siehe:
Einzelne Blockanweisung in einer Methode

An dieser Stelle verwende ich bereits die PSI-Knoten, denn ich möchte Whitespace-Informationen prüfen, die in UAST nicht zur Verfügung stehen. Dazu benötige ich die sourcePsi-Eigenschaft des UAST-Knotens auf Blockebene.

Ein Kommentar vor einer „Blockanweisung“

Ein Grenzfall ist, wenn sich ein Kommentar direkt über meiner Blockanweisung befindet. Hier möchte ich den Whitespace über dem Kommentar prüfen, so dass der erste relevante Whitespace über der Kommentaranweisung liegt.

  • Schließlich zähle ich die Anzahl der Zeilenumbrüche im relevanten Whitespace; wenn sie nicht größer als 1 ist, melde ich ein Problem.

Das Ergebnis

Als Ergebnis erwarte ich die folgenden Warnungen in der IDE, so dass es für jeden Entwickler klar ist, dass eine zusätzliche Zeile hinzugefügt werden muss. Ich kann diese Warnungen auch im Lint-Report erhalten, wenn ich den Task lint gradle ausführe. Außerdem kann ich sogar den Schweregrad auf Fehler erhöhen, wenn ich den Merge-Request-Job blockieren möchte.

IDE Warnung vor und nach Blockanweisung

Fazit

Nach der Integration der benutzerdefinierten Regel, müssen wir uns nicht mehr mit diesem frustrierenden Problem beschäftigen; Stattdessen können wir unsere Gehirnleistung darauf verwenden, komplexere Probleme zu finden, wenn wir den Code des anderen überprüfen. Noch besser ist, dass wir zu 100 % garantieren können, dass dieser Fehler nicht versehentlich in unsere Codebasis gelangt, weil wir unseren MR-Job so konfigurieren können, dass er fehlschlägt, wenn jemand es versäumt, diesen Fehler zu beheben.

Auch wenn Android Lint nicht primär für Formatierungsregeln gedacht ist, war es in meinem Fall sehr praktisch, weil es sowohl für Java als auch für Kotlin verwendet werden kann: kein doppeltes Schreiben, Pflegen und Integrieren von Regeln erforderlich.

Auf der anderen Seite müssen wir feststellen, dass diese Regel eine sehr einfache ist, da ich nur Whitespaces und geschweifte Klammern auf PSI-Ebene prüfen muss, die in beiden Sprachen gleich sind. Deshalb muss ich keinen sprachspezifischen Code schreiben. Sollte ich dennoch sprachspezifischen Code schreiben müssen (z. B. die Behandlung des Elvis-Operators in Kotlin oder des Ternary-Operators in Java), würde ich trotzdem in Erwägung ziehen, Android Lint gegenüber dem Schreiben eines einzelnen ktlint und einer Checkstyle-Regel zu bevorzugen, weil ich dann wahrscheinlich immer noch viel weniger Arbeit hätte.

Nächster Teil…

Wenn Ihnen der erste Teil des Artikels gefallen hat, lesen Sie bitte auch den zweiten Teil, in dem ich detailliert auf die Integration meiner Regel in Android- (und Nicht-Android-) Projekte eingehe, wie man die bereits bestehenden Probleme in unserer Codebasis behebt und wie wir unsere Regeln unit-testen und debuggen können.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.