Utilisation de clang-format et clang-tidy pour du code C

Utilisation de clang-format

L’intérêt d’utiliser un outil de formatage de code apparaît lorsque l’on travaille sur du code qui commence à être volumineux (> 1000 lignes) et avec plusieurs développeurs: on va chercher à faire en sorte que le code soit le plus lisible possible, le moins dépendant de l’éditeur de chacun ou de la façon de programmer de chacun… qu’il soit donc le plus uniforme possible. Ainsi chaque développeur perdra moins de temps à déchiffrer le code pour se concentrer sur la logique et les taches qui lui sont affectées. Au minimum, on cherchera à avoir la structure la plus lisible possible (via l’indentation des blocs) mais il est également possible de trier par ordre alphabétique les #include (pour les retrouver plus facilement), d’aligner les déclaration de variables…Comme par exemple sur le programme suivant:

#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


int main ()
{
DIR * folder;struct dirent *entry;
char folder_name[] = ".";
folder = opendir (folder_name);
if (folder == NULL)
{
perror ("Unable to read directory");
exit (-1);
}
while ((entry = readdir (folder)))
{
struct stat buffer; int status;
status = lstat (entry->d_name, &buffer);
if (S_ISREG(buffer.st_mode))
printf ("Fichier %s\n", entry->d_name);
}
closedir (folder);
return (0);
}
#include <dirent.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>

int main ()
{
    DIR *          folder;
    struct dirent *entry;
    char           folder_name[] = ".";
    folder                       = opendir (folder_name);
    if (folder == NULL)
    {
        perror ("Unable to read directory");
        exit (-1);
    }
    while ((entry = readdir (folder)))
    {
        struct stat buffer;
        int         status;
        status = lstat (entry->d_name, &buffer);
        if (S_ISREG (buffer.st_mode)) printf ("Fichier %s\n", entry->d_name);
    }
    closedir (folder);
    return (0);
}

Pour formater son code, on peut utiliser l’outil clang-format qui repose sur clang (il n’est pas installé par défaut, il faut installer le paquet via apt-get install clang-format sous debian). L’utilisation est relativement simple:

clang-format fichier.c                      # utilisation basique, le fichier formaté est affiché dans la sortie standard
clang-format -i fichier.c                   # le fichier passé paramètre est formaté
clang-format -i -style=Mozilla fichier.c    # on utilise la convention de formatage de Mozilla
clang-format -i -style=file fichier.c       # on utilise son propre fichier de conventions (suppose l'existance d'un fichier .clang-format)

Un exemple de fichier .clang-format est disponible que vous pouvez utiliser pour vos projets. De la même manière, si vous souhaitez utiliser clang-format pour de projets avec de nombreux fichiers ou pour l’utiliser en hook GIT ou en intégration continue (avec gitlab-ci par exemple), vous pouvez utiliser le script python disponible ici. Il suffit de lui passer en paramètres les répertoires contenants des fichiers .c et .h (avec l’option -r recursive).

./run-clang-format.py -r repertoire_1 repertoire_2 ... repertoire_n

Utilisation de clang-tidy

clang-tidy est un linter qui analyse votre code (sans le compiler) pour diagnostiquer les erreurs courantes de programmation qui ne sont pas forcément détéctées à la compilation. Par exemple soit le code suivant (extrait d’un projet étudiant):

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void lecture(char *nom_fichier,char capt[30],int nb_capt)
{
    FILE* fic ;
    char ligne[120];
    char *ptr_chaine ;
    short int num_ligne = 1 ;
    int nb_d=0;
    char anneetest[20];
    int annee;
    char moistest[20];
    char jourtest[20];
    char heuretest[20];
    char minutetest[20];
    char secondetest[20];

    fic = fopen( nom_fichier, "r") ;
    if (fic==NULL)
    {
        printf("Ouverture fichier impossible !");
        exit(0);
    }
    fgets( ligne, 120, fic);
    while ( fgets( ligne, 120, fic) != NULL )
    {

        num_ligne++ ;
        ptr_chaine = strtok (ligne, ",");
        if (sscanf(ptr_chaine,"%s", anneetest) != 1)
        {
            annee = atoi(anneetest);


        }
        ptr_chaine = strtok (NULL, ",");

        nb_d++;

    }
    fclose(fic);
}

La compilation semble que tout est OK:

$ gcc -c tidy.c

L’activation des warnings donne un peu plus de retours:

$ gcc -W -Wall -c tidy.c
tidy.c:16:10: warning: unused variable 'heuretest' [-Wunused-variable]
    char heuretest[20];
         ^
tidy.c:14:10: warning: unused variable 'moistest' [-Wunused-variable]
    char moistest[20];
         ^
tidy.c:17:10: warning: unused variable 'minutetest' [-Wunused-variable]
    char minutetest[20];
         ^
tidy.c:15:10: warning: unused variable 'jourtest' [-Wunused-variable]
    char jourtest[20];
         ^
tidy.c:18:10: warning: unused variable 'secondetest' [-Wunused-variable]
    char secondetest[20];
         ^
tidy.c:5:37: warning: unused parameter 'capt' [-Wunused-parameter]
void lecture(char *nom_fichier,char capt[30],int nb_capt)
                                    ^
tidy.c:5:50: warning: unused parameter 'nb_capt' [-Wunused-parameter]
void lecture(char *nom_fichier,char capt[30],int nb_capt)
                                                 ^
7 warnings generated.

En activant tous les tests de clang-tidy, on obtient beaaaaauccccouuup plus d’informations (un petit sous-ensemble est affiché à titre d’exemple):

$ clang-tidy --checks="*" tidy.c
/Users/jeremie/tmp/tidy.c:2:1: warning: #includes are not sorted properly [llvm-include-order]
#include <string.h>
^        ~~~~~~~~~~
         <stdlib.h>
/Users/jeremie/tmp/tidy.c:5:37: warning: parameter 'capt' is unused [misc-unused-parameters]
void lecture(char *nom_fichier,char capt[30],int nb_capt)
                                    ^
/Users/jeremie/tmp/tidy.c:5:42: warning: 30 is a magic number; consider replacing it with a named constant [cppcoreguidelines-avoid-magic-numbers]
void lecture(char *nom_fichier,char capt[30],int nb_capt)
                                         ^
/Users/jeremie/tmp/tidy.c:5:50: warning: parameter 'nb_capt' is unused [misc-unused-parameters]
void lecture(char *nom_fichier,char capt[30],int nb_capt)
                                                 ^
[...]
/Users/jeremie/tmp/tidy.c:20:31: warning: use 'fopen' mode 'e' to set O_CLOEXEC [android-cloexec-fopen]
    fic = fopen( nom_fichier, "r") ;
                              ^~~
                              "re"
[...]
/Users/jeremie/tmp/tidy.c:32:13: warning: Call to function 'sscanf' is insecure as it does not provide bounding of the memory buffer or security checks introduced in the C11 standard. Replace with analogous functions that support length arguments or provides boundary checks such as 'sscanf_s' in case of C11 [clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling]
        if (sscanf(ptr_chaine,"%s", anneetest) != 1)
            ^
[...]
/Users/jeremie/tmp/tidy.c:34:13: note: Value stored to 'annee' is never read
/Users/jeremie/tmp/tidy.c:34:21: warning: 'atoi' used to convert a string to an integer value, but function will not report conversion errors; consider using 'strtol' instead [cert-err34-c]
            annee = atoi(anneetest);
                    ^
/Users/jeremie/tmp/tidy.c:38:9: warning: Value stored to 'ptr_chaine' is never read [clang-analyzer-deadcode.DeadStores]
        ptr_chaine = strtok (NULL, ",");
        ^
/Users/jeremie/tmp/tidy.c:38:9: note: Value stored to 'ptr_chaine' is never read

On voit que clang-tidy nous alerte sur énormément de choses: l’utilisation de magic numbers (à la place de constantes), des valeurs inutilisées ou non lues, l’utilisation de fonctions non-sûres et recommande même l’utilisation de fonctions plutôt que d’autres. Son utilisation est relativement simple puisqu’il suffit de l’appeler en spécifiant les alertes que l’on veut utiliser (* = toutes les alertes):

$ clang-tidy --checks="*" *.c *.h

Autres outils

Sur ce même principe, d’autres outils existent comme cppcheck, oclint qui peuvent s’utiliser en complément et permettent d’avoir d’autres types d’alertes (en particulier cppcheck qui inclut les tests Misra et CERT très utilisés pour les systèmes embarqués). Pour un exemple d’utilisation de ces outils, je vous invite à consulter le dépôt gitlab qui contient un exemple simple de projet en C d’intégration continue.