Per Kotlin e Java in una volta sola
TL;DR
In questa prima parte dell’articolo, principalmente, voglio dimostrare che vale la pena scrivere una regola di analisi del codice personalizzata per far rispettare le convenzioni del codice. In secondo luogo, voglio dimostrare che oltre a Checkstyle e ktlint, anche Android Lint dovrebbe essere considerato quando si crea una regola di analisi del codice legata alla formattazione, nonostante questo non sia il suo scopo principale. Spiegherò il suo principale vantaggio nel mio caso rispetto agli altri strumenti, e descriverò il funzionamento interno che fornisce questo vantaggio. Per ispirazione, mostrerò anche i passi che ho fatto per far funzionare la mia regola di formattazione con Android Lint.
Motivazione
Sono frustrato quando ricevo commenti relativi alla formattazione sulle mie richieste di fusione o quando devo aggiungere tali commenti a quelle degli altri. A volte, dobbiamo cambiare rami per correggere un singolo errore di formattazione nel nostro MR, il che è abbastanza demotivante. Ecco perché ho iniziato a pensare a come potremmo automatizzare la correzione di questo piccolo (ma importante) problema, in modo da poterci concentrare su questioni più complesse durante le revisioni del codice.
Utilizziamo diversi strumenti lint, come ktlint o Checkstyle, per far rispettare le convenzioni globali di formattazione (ordine delle variabili, lunghezza delle linee, ecc.), ma questi non includono alcune regole a livello aziendale per default. Per questi casi, i suddetti strumenti supportano anche l’aggiunta di regole personalizzate. Ho deciso di scriverne una per gli errori di formattazione più comuni che facciamo:
Noi richiediamo sempre doppie interruzioni di riga prima e dopo le “istruzioni di blocco”, che possono essere blocchi if, switch (quando in Kotlin), for, try, o while, a meno che non siano esattamente all’inizio o alla fine di un metodo o di un altro blocco. Crediamo che questo renda il nostro codice più leggibile, ecco perché lasciamo sempre un commento durante la revisione del codice se qualcuno viola questa regola (e se ci capita di notarlo).
Qual è il miglior strumento per questo problema
Possiamo dire che ktlint per Kotlin e Checkstyle per Java sono i principali strumenti di analisi del codice di formattazione, ma ora sceglierò un altro strumento ancora: Android Lint (non solo per progetti Android), perché supporta anche la scrittura di regole personalizzate che possono essere applicate a Kotlin e Java allo stesso tempo. Sembra una scelta più logica, perché usiamo entrambi i linguaggi nei nostri progetti Android, e non volevo né scrivere la stessa regola due volte né mantenerle contemporaneamente. Per non parlare dell’integrazione della regola, che avrebbe anche dovuto essere fatta due volte.
Perché Java è ancora importante
Come sviluppatore Android, devo scrivere codice sia in Java che in Kotlin. Tuttavia, possiamo dire che non vale più la pena concentrarsi su Java, perché con l’arrivo di Kotlin, lo usiamo sempre di più al posto di Java nella nostra codebase. D’altra parte, credo che la transizione non stia avvenendo così rapidamente nei grandi progetti già in produzione. Quindi, siccome vogliamo vedere codice ben formattato anche in Java, è importante avere questa regola per entrambi i linguaggi.
Come scrivere regole di lint personalizzate
Ci sono molti tutorial e articoli sulla scrittura di regole di analisi del codice personalizzate, quindi non lo spiegherò troppo in dettaglio in questo post. Ma se siete interessati, ecco un paio di link che posso raccomandare per i diversi strumenti: A Checkstyle, il documento ufficiale, a ktlint e Android Lint, i grandi articoli mediatici di Niklas Baudy.
Come funzionano Android Lint e altri strumenti di analisi statica del codice
Inizialmente, Android Lint è stato creato per trovare principalmente problemi specifici di Android nel codice Java. Per analizzare il codice Java, Android Lint creava un albero sintattico astratto specifico per Java (o semplicemente AST, per saperne di più su Wikipedia), che è una rappresentazione ad albero del codice sorgente. Altri strumenti di analisi statica del codice utilizzano anche un AST specifico per la lingua per l’analisi;
Checkstyle: Java-, ktlint: Kotlin-, detekt: Kotlin-specifico. Gli strumenti attraversano questo AST e trovano gli errori controllando i suoi nodi e i loro attributi.
Come Android Lint può controllare due lingue contemporaneamente
La differenza tra Android Lint e altri strumenti è che quando anche Kotlin è diventato un linguaggio supportato per Android, Android Lint ha iniziato a supportare anche Kotlin per quanto riguarda le regole che avevamo già per Java. Per questo, hanno introdotto il cosiddetto Universal Abstract Syntax Tree (sviluppato da JetBrains) che fornisce una rappresentazione ad albero del codice che può essere applicata sia per Kotlin che per i linguaggi Java allo stesso tempo, quindi è ad un livello di astrazione superiore rispetto ad un AST specifico della lingua. Per maggiori informazioni, raccomando questa parte del discorso di Tor Norbye – il creatore e manutentore di Android Lint.
Sia UAST che AST forniscono dettagli di alto livello sul codice sorgente. Non contengono informazioni su spazi bianchi o parentesi graffe, ma di solito è sufficiente per le regole specifiche di Android. Vedi un esempio UAST qui sotto:
La libreria UAST include tutte le espressioni specifiche del linguaggio, ma fornisce anche interfacce comuni per esse. Esempio per l’espressione if:
Albero PSI (Program Structure Interface Tree)
L’albero PSI è costruito sull’UAST nel caso di Android Lint (nel caso di altri strumenti, è costruito sull’AST specifico del linguaggio), e questo è l’albero che contiene più dettagli sulla struttura del codice, come spazi bianchi, parentesi graffe ed elementi simili.
In Android Lint, le espressioni PSI sono incluse nell’implementazione delle espressioni UAST, che sono i nodi dell’UAST. Per esempio, org.jetbrains.kotlin.psi.KtIfExpression può essere accessibile da KotlinUIfExpression.
C’è anche un comodo plugin per intelliJ: PsiViewer che può facilitare il debug delle regole di analisi del codice basate sull’albero PSI. Vedi l’esempio qui sotto con lo stesso frammento di codice, dove possiamo vedere che i due linguaggi hanno diversi token specifici per la lingua negli alberi:
Usando sia UAST che PSI Tree
Ho bisogno sia di UAST che di PSI Tree per creare la mia regola di formattazione, perché UAST è ottimo per filtrare e visitare i nodi che mi interessano per entrambe le lingue contemporaneamente, ma come ho detto non fornisce informazioni sulla formattazione, es. informazioni sugli spazi bianchi che sono essenziali per me. UAST si concentra piuttosto su un livello di astrazione più alto, quindi principalmente non è per creare regole di formattazione.
Dal plugin Gradle 3.4 e la relativa versione 26.4.0 di Android Lint, possiamo ottenere la rappresentazione ad albero PSI di ogni nodo e dei suoi dintorni, non solo in Java, ma anche in Kotlin. Questo rende possibile l’uso di Android Lint per la mia regola di formattazione.
Come viene implementata la regola
Per prima cosa, ho bisogno di creare il mio problema, dove ho impostato lo scopo a JAVA_FILE, che significa sia Java che Kotlin in Android Lint attualmente. (Qui è dove possiamo impostare anche altri tipi di file come XML o file Gradle, perché Android Lint può controllare anche loro.)
Poi, nella mia classe rilevatore, Enumero i nodi che mi interessano all’interno della funzione getApplicableUastTypes, così Android Lint saprà che deve chiamare i relativi metodi di visita sovrascritti come visitForEachExpression. In ogni metodo di visita, chiamo semplicemente il mio metodo checker.
Nel mio metodo checker, il parametro forward descrive se voglio controllare l’interruzione di riga prima o dopo il “block statement”. Nel corpo del metodo, indago sul numero di spazi bianchi dell’interruzione di riga intorno alla dichiarazione di blocco che sto visitando. Per questo, faccio tre passi principali:
- Primo, con il metodo firstBlockLevelNode, controllo qual è il primo nodo di livello del blocco, attorno al quale voglio controllare gli spazi bianchi, perché se uso un blocco if per assegnare un valore a una variabile come questo,
Lint esaminerebbe gli spazi bianchi appena prima della parola chiave if, ma io sono interessato agli spazi bianchi prima della parola chiave val. Quindi, in questo caso, la prima dichiarazione a livello di blocco è l’assegnazione del valore che avvolge la mia dichiarazione if.
- In secondo luogo, nel metodo firstRelevantWhiteSpaceNode, controllo qual è il primo nodo di spazio bianco rilevante, dove dovremmo contare le interruzioni di riga. A volte non c’è alcuno spazio bianco rilevante da controllare, perché se il mio blocco è all’inizio o alla fine di un metodo o di altri blocchi, allora va bene e posso rinunciare a ulteriori indagini. Vedi:
A questo punto sto già usando i nodi PSI, perché voglio controllare le informazioni sugli spazi bianchi che non sono fornite in UAST. Per questo, ho bisogno di ottenere la proprietà sourcePsi del nodo UAST a livello di blocco.
Un caso limite è se c’è un commento proprio sopra il mio block statement. Qui, voglio controllare lo spazio bianco sopra il commento, quindi il primo spazio bianco rilevante è sopra la dichiarazione di commento.
- Infine, conto il numero di interruzioni di riga nello spazio bianco rilevante; se non è maggiore di 1, segnalo un problema.
Il risultato
Come risultato, mi aspetto i seguenti avvertimenti nell’IDE, così sarà chiaro per ogni sviluppatore che una linea aggiuntiva deve essere aggiunta. Posso anche ottenere questi avvertimenti nel rapporto Lint se eseguo lint gradle task. Inoltre, posso anche elevare la gravità del problema a errore, se voglio bloccare il lavoro di richiesta di unione.
Conclusione
Dopo aver integrato la regola personalizzata, non dobbiamo più concentrarci su questo frustrante problema; invece, possiamo spendere il nostro cervello per trovare problemi più complessi quando rivediamo il codice degli altri. Ancora meglio, possiamo garantire al 100% che questo errore non entrerà accidentalmente nel nostro codice, perché possiamo configurare il nostro lavoro MR in modo che fallisca quando qualcuno non riesce a risolvere questo problema.
Anche se Android Lint non è principalmente per le regole di formattazione, nel mio caso è stato abbastanza pratico, perché può essere usato sia per Java che per Kotlin: non è necessario scrivere, mantenere e integrare due regole.
D’altra parte, dobbiamo notare che questa regola è molto semplice in quanto devo solo controllare gli spazi bianchi e le parentesi graffe a livello di PSI, che sono gli stessi nei due linguaggi. Ecco perché non devo scrivere alcun codice specifico per la lingua. Tuttavia, se dovessi ancora scrivere del codice specifico per la lingua (ad esempio la gestione dell’operatore Elvis in Kotlin o dell’operatore Ternary in Java), considererei comunque di preferire Android Lint rispetto alla scrittura di ktlint one-one e di regole Checkstyle, perché probabilmente avrei ancora molto meno lavoro.
Parte successiva…
Se vi è piaciuta la prima parte dell’articolo, controllate anche la seconda parte, dove descriverò in dettaglio l’integrazione della mia regola nei progetti Android (e non-Android), come risolvere i problemi già esistenti nel nostro codebase, e come possiamo eseguire test unitari e debuggare le nostre regole.