🔝 Retour au Sommaire
L'exécution de clang-tidy sans configuration explicite active un jeu de checks par défaut limité. Pour exploiter pleinement l'outil, il faut sélectionner les checks adaptés à son projet, configurer leurs options, et partager cette configuration avec l'ensemble de l'équipe. C'est le rôle du fichier .clang-tidy : un fichier de configuration au format YAML, placé à la racine du projet et versionné dans Git, qui définit exactement quels checks sont actifs, comment ils se comportent, et quels warnings sont promus en erreurs.
Lorsque clang-tidy analyse un fichier source, il remonte l'arborescence des répertoires à partir du fichier analysé jusqu'à la racine du système de fichiers, et utilise le premier fichier .clang-tidy rencontré. Ce mécanisme de résolution hiérarchique permet d'avoir des configurations différentes par sous-répertoire.
mon_projet/
├── .clang-tidy ← Configuration principale du projet
├── src/
│ ├── core/
│ │ └── parser.cpp ← Utilise .clang-tidy de la racine
│ └── legacy/
│ ├── .clang-tidy ← Configuration spécifique (checks assouplis)
│ └── old_module.cpp ← Utilise .clang-tidy de src/legacy/
├── tests/
│ ├── .clang-tidy ← Configuration spécifique (moins stricte)
│ └── test_parser.cpp ← Utilise .clang-tidy de tests/
└── third_party/
├── .clang-tidy ← Configuration minimale ou vide
└── ... ← Code tiers non analysé
Ce mécanisme est particulièrement utile dans trois situations :
- Code legacy : un sous-répertoire contenant du code ancien peut avoir une configuration plus permissive pendant sa migration progressive, sans relâcher les exigences sur le code récent.
- Tests : le code de test a souvent des conventions différentes (magic numbers acceptés, variables inutilisées dans les fixtures). Un
.clang-tidydédié danstests/évite de polluer la configuration principale avec des exceptions. - Code tiers : le répertoire
third_party/ouvendor/contient du code que vous ne maintenez pas. Un fichier.clang-tidyminimal (ou avecChecks: '-*') y désactive toute analyse.
💡 Attention : contrairement à
.gitignore, les fichiers.clang-tidyne se combinent pas par héritage. Le fichier le plus proche du source analysé est le seul utilisé — il remplace entièrement le fichier parent, il ne le complète pas. Chaque.clang-tidydoit donc être autonome et complet.
Un fichier .clang-tidy est un document YAML avec les clés suivantes :
---
# Sélection des checks
Checks: >
-*,
bugprone-*,
performance-*,
modernize-*,
readability-*,
clang-analyzer-*,
-modernize-use-trailing-return-type
# Warnings traités comme des erreurs
WarningsAsErrors: ''
# Fichiers d'en-tête à filtrer
HeaderFilterRegex: '.*'
# Options spécifiques par check
CheckOptions:
readability-identifier-naming.ClassCase: CamelCase
readability-identifier-naming.FunctionCase: camelCase
readability-identifier-naming.VariableCase: lower_case
readability-identifier-naming.ConstantCase: UPPER_CASE
readability-function-cognitive-complexity.Threshold: 25
modernize-use-auto.MinTypeNameLength: 5
# Comportement système
SystemHeaders: false
UseColor: true Détaillons chaque clé.
C'est la clé la plus importante. Elle définit quels checks sont actifs via une liste de patterns séparés par des virgules. Les patterns sont évalués de gauche à droite, et chaque pattern active ou désactive les checks correspondants.
| Pattern | Effet |
|---|---|
bugprone-* |
Active tous les checks dont le nom commence par bugprone- |
-bugprone-* |
Désactive tous les checks bugprone-* |
modernize-use-nullptr |
Active ce check spécifique |
-modernize-use-trailing-return-type |
Désactive ce check spécifique |
* |
Active tous les checks |
-* |
Désactive tous les checks |
Les patterns sont appliqués séquentiellement. Le dernier pattern qui correspond à un check détermine s'il est actif ou inactif. Ce mécanisme permet une logique « tout activer sauf » ou « tout désactiver sauf » :
# Stratégie "opt-in" : partir de rien et activer explicitement
Checks: >
-*,
bugprone-*,
performance-*Ici, -* désactive d'abord tout, puis bugprone-* et performance-* réactivent ces deux catégories. Seuls les checks bugprone et performance sont actifs.
# Stratégie "opt-out" : tout activer et désactiver les indésirables
Checks: >
*,
-llvmlibc-*,
-fuchsia-*,
-altera-*,
-modernize-use-trailing-return-type,
-readability-identifier-lengthIci, * active tout, puis les patterns négatifs désactivent les catégories non pertinentes (checks spécifiques à des projets comme LLVM libc, Fuchsia, Altera) et les checks jugés trop bruyants pour ce projet.
Le caractère > en YAML introduit un folded scalar : les retours à la ligne sont convertis en espaces. Cela permet d'écrire la liste de checks sur plusieurs lignes pour la lisibilité, tout en produisant une seule chaîne au final :
# Ces deux écritures sont équivalentes :
# Multi-lignes (recommandé pour la lisibilité)
Checks: >
-*,
bugprone-*,
performance-*
# Ligne unique
Checks: '-*,bugprone-*,performance-*'La version multi-lignes est fortement recommandée pour la maintenabilité : chaque catégorie ou exception est visible sur sa propre ligne, ce qui facilite les revues de code et le suivi des modifications dans Git.
Cette clé utilise la même syntaxe de patterns que Checks pour promouvoir certains warnings en erreurs. Lorsqu'un check promu détecte un problème, clang-tidy retourne un code de sortie non nul, ce qui fait échouer le pipeline CI.
# Promouvoir les bugs graves en erreurs
WarningsAsErrors: >
bugprone-use-after-move,
bugprone-dangling-handle,
clang-analyzer-core.*,
clang-analyzer-cplusplus.NewDeleteLeaksCette mécanique permet une adoption progressive. Au début, tous les diagnostics sont des warnings : les développeurs voient les problèmes mais ne sont pas bloqués. Progressivement, les checks les plus critiques sont promus en erreurs, rendant impossible l'introduction de ces bugs dans la branche principale.
# Stratégie agressive : tous les checks actifs sont des erreurs
WarningsAsErrors: '*'Cette stratégie « zéro warning » est la plus stricte. Elle est adaptée aux nouveaux projets qui partent sur une base saine. Pour les projets existants avec des centaines de warnings, elle serait bloquante — la stratégie progressive est alors préférable.
Par défaut, clang-tidy ne signale les diagnostics que dans les fichiers source (.cpp) directement analysés, pas dans les headers inclus. La clé HeaderFilterRegex contrôle quels headers sont inclus dans l'analyse via une expression régulière appliquée au chemin du fichier :
# Analyser uniquement les headers du projet (pas les headers système ni tiers)
HeaderFilterRegex: 'src/|include/'
# Analyser tous les headers (y compris tiers — bruyant)
HeaderFilterRegex: '.*'
# Analyser les headers correspondant au nom du projet
HeaderFilterRegex: 'mon_projet/'La configuration recommandée filtre les headers pour n'inclure que ceux du projet. Analyser les headers système ou des librairies tierces produit un flot de diagnostics sur du code que vous ne contrôlez pas.
Beaucoup de checks sont paramétrables. La clé CheckOptions permet de personnaliser leur comportement. Les options sont spécifiées sous forme de paires clé-valeur, où la clé suit le format nom-du-check.NomOption :
Le check readability-identifier-naming est l'un des plus configurables. Il permet de définir une convention de nommage complète pour chaque catégorie d'identifiant :
CheckOptions:
# Classes et structs en CamelCase
readability-identifier-naming.ClassCase: CamelCase
readability-identifier-naming.StructCase: CamelCase
# Fonctions et méthodes en camelCase
readability-identifier-naming.FunctionCase: camelCase
readability-identifier-naming.MethodCase: camelCase
# Variables locales en snake_case
readability-identifier-naming.VariableCase: lower_case
readability-identifier-naming.ParameterCase: lower_case
# Membres privés avec suffixe _
readability-identifier-naming.PrivateMemberCase: lower_case
readability-identifier-naming.PrivateMemberSuffix: '_'
# Constantes et enums en UPPER_CASE
readability-identifier-naming.ConstantCase: UPPER_CASE
readability-identifier-naming.EnumConstantCase: UPPER_CASE
# Namespaces en lower_case
readability-identifier-naming.NamespaceCase: lower_case
# Template parameters en CamelCase
readability-identifier-naming.TemplateParameterCase: CamelCaseLes valeurs acceptées pour les styles de casse sont : lower_case, UPPER_CASE, camelCase, CamelCase, camel_Snake_Case, Camel_Snake_Case, aNy_CasE.
CheckOptions:
# Seuil de complexité cognitive (défaut : 25)
readability-function-cognitive-complexity.Threshold: 30
# Longueur de ligne maximale pour readability-function-size
readability-function-size.LineThreshold: 100
# Nombre maximal de paramètres
readability-function-size.ParameterThreshold: 6
# Taille minimale du nom de type pour modernize-use-auto
modernize-use-auto.MinTypeNameLength: 5CheckOptions:
# Activer l'analyse des expressions dans bugprone-assert-side-effect
bugprone-assert-side-effect.AssertMacros: 'assert,ASSERT,Q_ASSERT'
# Types considérés comme "trivialement copiables" pour performance-*
performance-unnecessary-value-param.AllowedTypes: 'std::string_view;std::span'
# Inclure les fonctions de test dans cert-err58-cpp
cert-err58-cpp.CheckFunctionsWithoutThrow: truePour découvrir les options disponibles pour un check, consultez la documentation LLVM du check concerné. Chaque page de documentation liste les options avec leurs valeurs par défaut.
# false (défaut) : ne pas analyser les headers système
SystemHeaders: falseLorsque cette clé est à true, clang-tidy analyse également les headers système (/usr/include/, headers de la STL). C'est presque toujours indésirable — les headers système déclenchent des centaines de diagnostics hors de votre contrôle. Conservez la valeur par défaut false.
Configuration stricte pour un projet qui démarre sur une base saine :
---
Checks: >
-*,
bugprone-*,
clang-analyzer-*,
concurrency-*,
cppcoreguidelines-*,
misc-*,
modernize-*,
performance-*,
readability-*,
-modernize-use-trailing-return-type,
-readability-identifier-length,
-cppcoreguidelines-avoid-magic-numbers,
-readability-magic-numbers,
-misc-non-private-member-variables-in-classes
WarningsAsErrors: >
bugprone-*,
clang-analyzer-*
HeaderFilterRegex: 'src/|include/'
CheckOptions:
readability-identifier-naming.ClassCase: CamelCase
readability-identifier-naming.FunctionCase: lower_case
readability-identifier-naming.VariableCase: lower_case
readability-identifier-naming.PrivateMemberSuffix: '_'
readability-identifier-naming.ConstantCase: UPPER_CASE
readability-identifier-naming.NamespaceCase: lower_case
readability-function-cognitive-complexity.Threshold: 25
modernize-use-auto.MinTypeNameLength: 5
performance-move-const-arg.CheckTriviallyCopyableMove: falseLes catégories bugprone-* et clang-analyzer-* sont promues en erreurs : aucun bug de ces catégories ne peut être introduit. Les checks cosmétiques comme modernize-use-trailing-return-type (controversial) et readability-identifier-length (trop bruyant sur les variables de boucle) sont désactivés.
Configuration plus permissive, adaptée à un projet existant où l'activation de tous les checks produirait des milliers de warnings :
---
Checks: >
-*,
bugprone-*,
clang-analyzer-core.*,
clang-analyzer-cplusplus.*,
performance-unnecessary-value-param,
performance-unnecessary-copy-initialization,
performance-for-range-copy,
modernize-use-nullptr,
modernize-use-override,
modernize-use-emplace,
readability-redundant-string-cstr
WarningsAsErrors: ''
HeaderFilterRegex: 'src/'
CheckOptions:
readability-function-cognitive-complexity.Threshold: 50Seuls les checks les plus impactants sont activés. Aucun n'est promu en erreur — la stratégie est de corriger progressivement. Le seuil de complexité cognitive est relevé à 50 (contre 25 par défaut) pour ne pas signaler toutes les fonctions héritées du code legacy. À mesure que le code est refactorisé, la configuration est durcie.
Configuration allégée pour le code de test :
---
Checks: >
-*,
bugprone-*,
clang-analyzer-*,
-bugprone-easily-swappable-parameters,
-cppcoreguidelines-avoid-magic-numbers,
-readability-magic-numbers,
-readability-function-cognitive-complexity
WarningsAsErrors: ''
HeaderFilterRegex: 'tests/'
CheckOptions: {}Les checks sur les magic numbers et la complexité des fonctions sont désactivés — les tests utilisent légitimement des valeurs littérales et les fixtures de test peuvent être longues. Les checks bugprone-* et clang-analyzer-* restent actifs car les bugs dans le code de test sont tout aussi problématiques que dans le code de production.
---
Checks: '-*'Ce fichier d'une seule ligne, placé dans third_party/, désactive complètement l'analyse. C'est la configuration recommandée pour le code tiers : vous ne le maintenez pas, les diagnostics ne sont pas actionnables.
Pour vérifier quels checks sont effectivement actifs avec la configuration courante :
clang-tidy --list-checks -p build/ src/main.cppCette commande lit le .clang-tidy applicable au fichier spécifié et affiche la liste des checks actifs. C'est un outil de diagnostic indispensable lorsqu'un check semble ne pas se déclencher : peut-être est-il désactivé par un pattern négatif plus tardif dans la chaîne Checks.
Pour afficher la configuration complète telle qu'elle est interprétée :
clang-tidy --dump-config -p build/ src/main.cppCette commande affiche le YAML complet résolu, incluant toutes les options par défaut de chaque check actif. C'est particulièrement utile pour découvrir les options non documentées ou les valeurs par défaut.
Le fichier .clang-tidy fait partie intégrante du code source du projet. Il doit être :
- Versionné dans Git au même titre que le
CMakeLists.txtou leMakefile. - Revu en code review lors de chaque modification. L'activation ou la désactivation d'un check affecte la qualité du code sur l'ensemble du projet — c'est une décision d'équipe, pas une préférence individuelle.
- Documenté par des commentaires YAML expliquant les choix non évidents, en particulier les désactivations :
Checks: >
-*,
bugprone-*,
performance-*,
# Désactivé : trop de faux positifs sur nos fonctions template
-bugprone-easily-swappable-parameters,
# Désactivé : incompatible avec notre convention de trailing return types
-modernize-use-trailing-return-typeLa stabilité de la configuration est importante. Changer fréquemment les checks actifs crée du bruit dans le diff et frustre les développeurs. La stratégie recommandée est de définir la configuration en début de projet (ou lors d'une session dédiée pour un projet existant), puis de la faire évoluer de façon incrémentale et délibérée : un check ajouté par sprint, avec les corrections associées dans un commit dédié.