Mesurer et améliorer la qualité du code R, pas à pas
rsonar est l’équivalent R de SonarQube : il centralise la mesure de la qualité du code R dans un rapport unique et interactif.
| Outil | Rôle |
|---|---|
lintr |
Analyse statique — bugs, style, complexité |
styler |
Formatage du code |
covr |
Couverture de tests |
goodpractice |
Bonnes pratiques de packaging |

SQALE = Software Quality Assessment based on Lifecycle Expectations.
C’est la méthode popularisée par SonarQube — et reprise par rsonar — pour transformer les défauts du code en coût estimé, exprimé en minutes.
Quatre notions clés :
Chaque issue a un coût de remédiation : le temps estimé pour la corriger, en minutes.
| Issue | Pénalité par défaut |
|---|---|
Erreur lintr |
30 min |
Avertissement lintr |
10 min |
| Violation de style | 2 min |
Fichier mal formaté (styler) |
5 min |
Échec goodpractice |
20 min |
| Point de couverture manquant | 5 min / point |
La dette totale = somme de ces pénalités.
On rapporte la dette totale à un effort de référence :
D_total : somme des pénalités de toutes les issuesfichiers × 30 min : effort estimé pour développer les fichiersLe score qualité s’en déduit :
Le ratio est converti en note de maintenabilité :
| Ratio | Note | Signification |
|---|---|---|
| < 5 % | A | Excellente |
| 5 – 10 % | B | Bonne |
| 10 – 20 % | C | Acceptable |
| 20 – 50 % | D | Médiocre |
| > 50 % | E | Critique |
Effort de référence : SonarQube → lignes de code × 30 min ; rsonar → fichiers × 30 min.
Exemple : 2 erreurs + 2 warnings + 5 styles = 90 min, rapportées à 300 min (10 fichiers) → ratio 30 % → note D.
Une démarche en boucle, sur un exemple fil rouge :
rsonar (score, note, dette)Structure du projet :
├── 01_assignment_and_formatting.R
├── 02_boolean_and_naming.R
├── 03_cyclomatic_complexity.R
├── 04_unused_vars_and_syntax.R
├── 05_non_standard_evaluation.R
├── 06_function_length.R
├── 07_string_quote_and_infix.R
├── 08_unnecessary_concatenation.R
├── 09_implicit_returns_and_braces.R
└── 10_duplicate_code_and_magic_numbers.R
Exemple 01_assignment_and_formatting.R :
# File 1: Assignment operator & formatting issues
Part_SAU = function( SAU_HA = 80 , surface_totale = 100 , unused_arg = 10 ){
var_inutile1 = 10
var_inutile2 = 20
if( SAU_HA == NA || surface_totale == NA || SAU_HA == NULL ){ return( NULL ) }
for( i in 1:length( SAU_HA ) ){ print( i ) }
msg = paste( "Part SAU: " , SAU_HA , sep = "" )
if( class( SAU_HA ) == "numeric" ){ warning( msg ) }
return( round( res , 1 ) )
print("Fini"
}Problèmes : nommage, espaces, = vs <-, pas de doc, ligne trop longue, aucun test…
Affiche un rapport interactif global.
sonar_fix() s’appuie sur air, le formateur R rapide de Posit.
── rsonar Fix (dry-run) ──────────────────────────────────────────────────────────────────────────────────────────────────
ℹ Path: /var/data/nfs/CERISE/00-Espace-Personnel/damien.dotta/rsonar_tests
ℹ 10 R file(s) found
✔ Report saved: sonar-fixes.json
── rsonar Fix Report (dry-run - no files modified) ───────────────────────────────────────────────────────────────────────
ℹ Path : /var/data/nfs/CERISE/00-Espace-Personnel/damien.dotta/rsonar_tests
ℹ Files : 10 scanned, 10 modified
ℹ Time : 1.8s
── Fixes applied
styler: 8fixes to 10 file(s)
spacing: 3
true_false: 3
commas: 1
cleanup: 35
assignment: 4
unused_vars: 3
ℹ Run `sonar_fix()` with `dry_run = FALSE` to apply fixes.
✔ Applying fixes to 10 file(s) [2s]Avec dry_run à FALSE, les scripts sont modifiés :
# File 1: Assignment operator & formatting issues
Part_SAU = function( SAU_HA = 80 , surface_totale = 100 , unused_arg = 10 ){
if( SAU_HA == NA || surface_totale == NA || SAU_HA == NULL ){ return( NULL ) }
for( i in 1:length( SAU_HA ) ){ print( i ) }
msg = paste( "Part SAU: " , SAU_HA , sep = "" )
if( class( SAU_HA ) == "numeric" ){ warning( msg ) }
if( SAU_HA > 0 && surface_totale > 0 ){
res <- ( SAU_HA / surface_totale ) * 100
}
return( round( res , 1 ) )
print("Fini"
}C’est mieux mais tout n’est pas réglé.

Sur notre script exemple :

Améliorations manuelles :
Part_SAU → calculer_part_sau et SAU_HA → sau_ha (snake_case)SAU_HA == NA → is.na() et class() == "numeric" → is.numeric()for (i in 1:length(...)) print(i)print("Fini" et revenir au retour implicite#' Calculer la part de Surface Agricole Utile (SAU)
#'
#' @param sau_ha Numérique ou vecteur numérique. La SAU en hectares.
#' @param surface_totale Numérique ou vecteur numérique. La surface totale en ha.
#'
#' @return La part de SAU en pourcentage.
#' @export
calculer_part_sau <- function(sau_ha = 80, surface_totale = 100) {
if (is.null(sau_ha) || is.null(surface_totale)) {
return(NULL)
}
if (any(is.na(sau_ha)) || any(is.na(surface_totale))) {
return(NULL)
}
if (any(sau_ha < 0) || any(surface_totale <= 0)) {
stop("Les surfaces doivent être positives et la surface totale > 0.")
}
res <- (sau_ha / surface_totale) * 100
if (any(res > 100)) {
warning("Au moins une valeur de SAU dépasse la surface totale.")
}
return(res)
}

tests/testthat/test-calculer-part-sau.R :
library(testthat)
test_that("calculer_part_sau calcule la part de SAU sur un vecteur", {
res <- calculer_part_sau(sau_ha = c(80, 120), surface_totale = 200)
expect_equal(res, c(40, 60))
})
test_that("calculer_part_sau applique les valeurs par défaut", {
res <- calculer_part_sau()
expect_equal(res, 80)
})sonar_analyse(".") sans include_coverage = FALSE mesure la couverture dès qu’un dossier tests/ existe. Ici, elle n’est que de 7 % : une seule fonction est testée sur les 10 scripts.
La pénalité de couverture s’applique alors :
Cette pénalité dépasse à elle seule l’effort de référence (11 fichiers × 30 = 330 min) :
→ Score 0 % · Note E. C’est la couverture globale du projet qui compte, pas celle d’un seul fichier.
| Étape | Action | Score | Note | Couverture |
|---|---|---|---|---|
| 0 | Code initial | 0 % | E | 0 % |
| 1 | Style auto (sonar_fix) |
0 % | E | 0 % |
| 2 | Refactorisation + doc | 49 % | E | 0 % |
| 3 | Tests ajoutés sans pénalité | 50 % | D | 0 % |
| 3 | Tests ajoutés avec pénalité | 0 % | E | 7 % |
| 4 | Dépôt complet | 98 % | A | 100 % |
Chaque amélioration fait monter le score et baisser la dette.
Pourquoi la couverture ne bouge qu’à l’étape 3 ?
La couverture n’est mesurée que lorsqu’il existe des tests. Avant (étapes 0 à 2), elle vaut 0 % et n’engendre aucune pénalité. À l’étape 3, mesurer la couverture (7 %) déclenche la pénalité (80 − 7) × 5 = 365 min, qui fait retomber le score à 0 %.
⚠️ Pour l’éviter, il faut couvrir tout le projet (couverture ≥ 80 %), pas une seule fonction. Sinon, c’est la couverture globale qui fait chuter le score.
Le package rsonar peut être intégré dans un pipeline CI/CD pour automatiser la mesure de la qualité et le reformatage du code. Voir ce dépôt
Résultat : plus besoin de reformater son code à la main avant de commiter, ni de faire des allers-retours en revue de code sur de simples questions de style. Un développeur déclenche le job, et une MR toute prête arrive avec le diff de mise en forme et il ne reste plus qu’à la relire et la fusionner.
Extrait minimal de .gitlab-ci.yml :
# Comparer deux analyses (avant / après)
sonar_diff(res_v3, res_v0)
# Historiser les scores pour tracer la tendance
sonar_trend(res, file = "rsonar-history.json")sonar_diff() : nouveaux problèmes, problèmes corrigéssonar_trend() : courbe d’évolution de la noteexport_junit(res, "junit-results.xml") # GitLab/Jenkins
export_sarif(res, "results.sarif") # GitHub Code Scanning
export_sonar_json(res, "sonar-issues.json") # SonarQubeFormats standards : JUnit XML, SARIF 2.1.0, SonarQube Generic Issue Import.
rsonar = mesurer, corriger, sécuriser :
quality_score() et debt_index() pour un feedback immédiatsonar_analyse() + sonar_report() + sonar_hotspots()sonar_diff() / sonar_trend() pour voir la note progresserquality_gate() + exports + sonar_fix(create_mr = TRUE)Documentation : ddotta.github.io/rsonar · intégration CI/CD