// ⛔ INTERDIT : unwrap_or / unwrap_or_default / unwrap_or_else / unwrap / expect / anyhow / opérateur ? / await (sans match)
//
// detecteur-lock-order — détecte 3 classes de risques d'interblocage sur des
// structures concurrentes (carte partagée type DashMap, verrou type Mutex)
// dans du code source Rust. Stdlib uniquement (std::collections inclus),
// zéro dépendance externe, zéro appel réseau.
//
// R1 REENTRANT-GUARD : un guard (`let g = X.get(...)`/`X.get_mut(...)`/
// `X.entry(...)`) reste vivant pendant qu'un second
// appel sur la MÊME structure X survient avant tout
// `drop(g)` — upgrade lecture→écriture interdit.
// R2 HEAVY-UNDER-LOCK : un guard de verrou (`let g = X.lock()`) reste
// vivant pendant qu'une opération jugée lourde
// (hachage/dérivation, I/O fichier, réseau bloquant,
// verrou imbriqué sur un AUTRE identifiant) survient
// dans sa portée avant tout `drop(g)`.
// R3 LOCK-ORDER-INCONSISTENT : deux fonctions verrouillent la même paire
// d'identifiants dans un ordre inversé — risque
// classique d'interblocage entre threads
// concurrents.
//
// Analyse LEXICALE par portée de bloc (accolades), PAS une analyse de
// lifetime réelle du compilateur — un guard renommé via un pattern complexe
// (destructuration, tuple) peut échapper à R1/R2. R3 corrèle par
// IDENTIFIANT TEXTUEL du récepteur, même limite assumée que les autres
// skills du projet fondés sur une corrélation lexicale. Voir SKILL.md
// "Limites connues" pour le détail complet.
//
// Suivi de profondeur d'accolades/parenthèses caractère par caractère avec
// exclusion des chaînes de caractères (normales + brutes r#"..."#) et des
// commentaires — même technique que les autres skills Rust du projet.
//
// Substitution délibérée (même raison que les autres skills Rust du
// projet) : `eprintln!` remplace `tracing` — outil CLI ponctuel, pas un
// serveur long-running.
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
const DOSSIERS_IGNORES: &[&str] = &["target", ".git", "node_modules"];
const MARQUEUR_EXEMPTION_LIGNE: &str = "lock-order-ok";
const MOTIFS_GUARD_CARTE: &[&str] = &["get(", "get_mut(", "entry("];
const MOTIFS_CARTE_REENTRANTS: &[&str] =
&["get(", "get_mut(", "entry(", "insert(", "remove(", "contains_key("];
const MOTIF_VERROU: &str = "lock(";
const MOTIFS_LOURDS: &[&str] =
&["argon2", "blake3::", "std::fs::", "fs::", "reqwest::blocking"];
const LARGEUR_FENETRE_MAX: usize = 2000;
#[derive(Debug)]
enum Erreur {
ArgumentManquant,
DossierInvalide(String),
LectureFichier { chemin: String, source: std::io::Error },
}
impl std::fmt::Display for Erreur {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Erreur::ArgumentManquant => write!(f, "argument dossier_cible manquant"),
Erreur::DossierInvalide(chemin) => write!(f, "dossier invalide : {chemin}"),
Erreur::LectureFichier { chemin, source } => {
write!(f, "lecture de {chemin} echouee : {source}")
}
}
}
}
#[derive(Debug, Clone)]
struct Trouvaille {
regle: &'static str,
fichier: String,
ligne_origine: usize,
ligne_conflit: usize,
identifiant: String,
extrait_origine: String,
extrait_conflit: String,
}
fn correspond_a(motif: &str, caracteres: &[char], position: usize) -> bool {
let motif_car: Vec<char> = motif.chars().collect();
if position + motif_car.len() > caracteres.len() {
return false;
}
caracteres[position..position + motif_car.len()] == motif_car[..]
}
/// Fonction PURE — vrai si le texte de code (masque compris) se terminant
/// juste avant `fin` (exclu) correspond exactement à `motif`.
fn se_termine_par(caracteres: &[char], masque: &[bool], fin: usize, motif: &str) -> bool {
let motif_car: Vec<char> = motif.chars().collect();
if motif_car.len() > fin {
return false;
}
let debut = fin - motif_car.len();
for k in debut..fin {
if !masque[k] {
return false;
}
}
caracteres[debut..fin] == motif_car[..]
}
/// Si la position `i` est le début d'une chaîne de caractères (normale ou
/// brute) ou d'un commentaire (`//` ou `/* */`), retourne la position juste
/// après la fin de cette construction. Sinon None. Fonction PURE.
fn sauter_chaine_ou_commentaire(caracteres: &[char], i: usize) -> Option<usize> {
if correspond_a("//", caracteres, i) {
let mut j = i;
while j < caracteres.len() && caracteres[j] != '\n' {
j += 1;
}
return Some(j);
}
if correspond_a("/*", caracteres, i) {
let mut j = i + 2;
while j + 1 < caracteres.len() && !(caracteres[j] == '*' && caracteres[j + 1] == '/') {
j += 1;
}
return Some((j + 2).min(caracteres.len()));
}
if caracteres[i] == 'r' {
let mut nb_dieses = 0usize;
let mut j = i + 1;
while j < caracteres.len() && caracteres[j] == '#' {
nb_dieses += 1;
j += 1;
}
if j < caracteres.len() && caracteres[j] == '"' {
j += 1;
loop {
if j >= caracteres.len() {
return Some(j);
}
if caracteres[j] == '"' {
let mut compte = 0usize;
let mut k = j + 1;
while k < caracteres.len() && caracteres[k] == '#' && compte < nb_dieses {
compte += 1;
k += 1;
}
if compte == nb_dieses {
return Some(k);
}
}
j += 1;
}
}
}
if caracteres[i] == '"' {
let mut j = i + 1;
while j < caracteres.len() {
if caracteres[j] == '\\' {
j += 2;
continue;
}
if caracteres[j] == '"' {
return Some(j + 1);
}
j += 1;
}
return Some(j);
}
None
}
/// Construit, pour tout le fichier, un masque "ce caractère appartient-il à
/// du vrai code" (false = à l'intérieur d'une chaîne ou d'un commentaire).
/// Fonction PURE — une seule passe, réutilisée par toutes les recherches.
fn construire_masque_code(caracteres: &[char]) -> Vec<bool> {
let mut masque = vec![true; caracteres.len()];
let mut i = 0usize;
while i < caracteres.len() {
match sauter_chaine_ou_commentaire(caracteres, i) {
Some(fin) => {
let borne = fin.min(caracteres.len());
for m in masque.iter_mut().take(borne).skip(i) {
*m = false;
}
i = fin;
}
None => i += 1,
}
}
masque
}
/// Fonction PURE. `limite` est TOUJOURS explicite (jamais implicitement
/// "fin du fichier") — voir les skills soeurs pour la justification perf.
fn trouver_prochaine_occurrence(
caracteres: &[char], masque: &[bool], depart: usize, limite: usize, motif: &str,
) -> Option<usize> {
let borne = limite.min(caracteres.len());
let mut i = depart;
while i < borne {
if masque[i] && correspond_a(motif, caracteres, i) {
return Some(i);
}
i += 1;
}
None
}
/// À partir d'une position de départ, avance jusqu'à l'accolade ouvrante
/// d'un bloc, puis retourne les positions de début et de fin du corps.
/// Fonction PURE.
fn trouver_bornes_bloc(caracteres: &[char], masque: &[bool], depart: usize) -> Option<(usize, usize)> {
let mut i = depart;
let mut profondeur_parenthese = 0i32;
let mut position_ouverture: Option<usize> = None;
while i < caracteres.len() {
if !masque[i] {
i += 1;
continue;
}
match caracteres[i] {
'(' => profondeur_parenthese += 1,
')' => profondeur_parenthese -= 1,
';' if profondeur_parenthese == 0 => return None,
'{' if profondeur_parenthese == 0 => {
position_ouverture = Some(i);
break;
}
_ => {}
}
i += 1;
}
let debut_corps = match position_ouverture {
Some(p) => p + 1,
None => return None,
};
let mut profondeur_accolade = 1i32;
let mut k = debut_corps;
while k < caracteres.len() {
if !masque[k] {
k += 1;
continue;
}
match caracteres[k] {
'{' => profondeur_accolade += 1,
'}' => {
profondeur_accolade -= 1;
if profondeur_accolade == 0 {
return Some((debut_corps, k + 1));
}
}
_ => {}
}
k += 1;
}
None
}
/// Fonction PURE — retrouve chaque fonction (`fn`/`async fn`/méthode) du
/// fichier : ligne de début, et bornes de son corps.
fn extraire_fonctions(caracteres: &[char], masque: &[bool]) -> Vec<(usize, usize, usize)> {
let mut fonctions = Vec::new();
let mut curseur = 0usize;
while let Some(pos_fn) = trouver_prochaine_occurrence(caracteres, masque, curseur, caracteres.len(), "fn ") {
let apres_fn = pos_fn + 3;
let mut apres_nom = apres_fn;
while apres_nom < caracteres.len()
&& (caracteres[apres_nom].is_alphanumeric() || caracteres[apres_nom] == '_')
{
apres_nom += 1;
}
match trouver_bornes_bloc(caracteres, masque, apres_nom) {
Some((debut_corps, fin_corps)) => {
let ligne_debut = ligne_de_position(caracteres, pos_fn);
fonctions.push((ligne_debut, debut_corps, fin_corps));
curseur = fin_corps;
}
None => curseur = apres_nom,
}
}
fonctions
}
/// Fonction PURE — à partir de la position du `.` d'un appel comme
/// `carte.get()`, remonte pour extraire l'identifiant récepteur
/// (`carte`, `self.carte`...). None si le récepteur n'est pas un simple
/// enchaînement d'identifiants (ex. un appel de fonction imbriqué).
fn extraire_identifiant_recepteur(caracteres: &[char], masque: &[bool], position_point: usize) -> Option<String> {
if position_point == 0 {
return None;
}
let mut debut = position_point;
while debut > 0 {
let c = caracteres[debut - 1];
if masque[debut - 1] && (c.is_alphanumeric() || c == '_' || c == '.') {
debut -= 1;
} else {
break;
}
}
if debut == position_point {
return None;
}
let identifiant: String = caracteres[debut..position_point].iter().collect();
if identifiant.is_empty() {
return None;
}
Some(identifiant)
}
/// Fonction PURE — si le texte de code juste avant `debut_recepteur`
/// correspond à un patron de liaison `let NOM = ` / `let mut NOM = ` /
/// `let Some(NOM) = ` / `if let Some(NOM) = `, retourne `NOM`. Sinon None
/// (appel utilisé en ligne, aucun guard persistant à surveiller).
fn detecter_binding_avant(caracteres: &[char], masque: &[bool], debut_recepteur: usize) -> Option<String> {
let mut i = debut_recepteur;
while i > 0 && masque[i - 1] && caracteres[i - 1].is_whitespace() {
i -= 1;
}
if i == 0 || !masque[i - 1] || caracteres[i - 1] != '=' {
return None;
}
i -= 1;
if i > 0
&& masque[i - 1]
&& (caracteres[i - 1] == '=' || caracteres[i - 1] == '!' || caracteres[i - 1] == '<' || caracteres[i - 1] == '>')
{
return None;
}
while i > 0 && masque[i - 1] && caracteres[i - 1].is_whitespace() {
i -= 1;
}
// Patron "Some(NOM)" — if let / let-else
if i > 0 && masque[i - 1] && caracteres[i - 1] == ')' {
let fin_ident = i - 1;
let mut j = fin_ident;
while j > 0 && masque[j - 1] && (caracteres[j - 1].is_alphanumeric() || caracteres[j - 1] == '_') {
j -= 1;
}
if j == fin_ident {
return None;
}
let nom: String = caracteres[j..fin_ident].iter().collect();
if !se_termine_par(caracteres, masque, j, "Some(") {
return None;
}
// `debut_some` pointe déjà juste après l'espace séparant "let"/"if
// let" de "Some(" — ne PAS re-sauter d'espace avant de comparer,
// sinon la comparaison se décale d'un caractère et "let " ne
// correspond jamais (bug corrigé après échec des tests).
let debut_some = j - "Some(".chars().count();
if se_termine_par(caracteres, masque, debut_some, "let ") {
return Some(nom);
}
return None;
}
// Patron "NOM" simple — let NOM = / let mut NOM =
let fin_ident = i;
let mut j = i;
while j > 0 && masque[j - 1] && (caracteres[j - 1].is_alphanumeric() || caracteres[j - 1] == '_') {
j -= 1;
}
if j == fin_ident {
return None;
}
let nom: String = caracteres[j..fin_ident].iter().collect();
// `j` pointe déjà juste après l'espace séparant "let"/"mut" du nom —
// même correction que ci-dessus, aucun saut d'espace supplémentaire.
let mut k = j;
if se_termine_par(caracteres, masque, k, "mut ") {
k -= "mut ".chars().count();
}
if se_termine_par(caracteres, masque, k, "let ") {
return Some(nom);
}
None
}
fn ligne_de_position(caracteres: &[char], position: usize) -> usize {
1 + caracteres[..position].iter().filter(|c| **c == '\n').count()
}
fn extraire_ligne(caracteres: &[char], position: usize) -> String {
let mut debut = position;
while debut > 0 && caracteres[debut - 1] != '\n' {
debut -= 1;
}
let mut fin = position;
while fin < caracteres.len() && caracteres[fin] != '\n' {
fin += 1;
}
let mut extrait: String = caracteres[debut..fin].iter().collect();
extrait = extrait.trim().to_string();
if extrait.len() > 120 {
extrait.truncate(117);
extrait.push_str("...");
}
extrait
}
/// Une occurrence de guard trouvée dans une fonction : structure verrouillée,
/// variable liée (si liaison détectée), position juste après l'appel.
struct OccurrenceGuard {
identifiant: String,
variable_liee: Option<String>,
position_apres_appel: usize,
}
/// Fonction PURE — recense, dans la portée `[debut_corps, fin_corps)`, tous
/// les appels `X.motif(` (X = identifiant simple) pour les motifs fournis.
fn recenser_appels(
caracteres: &[char], masque: &[bool], debut_corps: usize, fin_corps: usize, motifs: &[&str],
) -> Vec<OccurrenceGuard> {
let mut occurrences = Vec::new();
for motif in motifs {
let mut curseur = debut_corps;
while let Some(position_point_motif) =
trouver_prochaine_occurrence(caracteres, masque, curseur, fin_corps, &format!(".{motif}"))
{
let position_point = position_point_motif; // position du '.'
curseur = position_point + 1;
let identifiant = match extraire_identifiant_recepteur(caracteres, masque, position_point) {
Some(id) => id,
None => continue,
};
let position_debut_recepteur = position_point - identifiant.chars().count();
let variable_liee = detecter_binding_avant(caracteres, masque, position_debut_recepteur);
let position_apres_appel = position_point + 1 + motif.chars().count();
occurrences.push(OccurrenceGuard {
identifiant,
variable_liee,
position_apres_appel,
});
}
}
occurrences.sort_by_key(|o| o.position_apres_appel);
occurrences
}
/// R1 — réentrance sur une carte partagée sans `drop()` du guard précédent.
fn detecter_reentrance_carte(caracteres: &[char], masque: &[bool], debut_corps: usize, fin_corps: usize) -> Vec<(usize, usize, String, String, String)> {
let mut resultats = Vec::new();
let guards = recenser_appels(caracteres, masque, debut_corps, fin_corps, MOTIFS_GUARD_CARTE);
let reentrants = recenser_appels(caracteres, masque, debut_corps, fin_corps, MOTIFS_CARTE_REENTRANTS);
for guard in &guards {
let nom_variable = match &guard.variable_liee {
Some(n) => n,
None => continue, // usage en ligne, pas de guard persistant
};
let zone_fin = (guard.position_apres_appel + LARGEUR_FENETRE_MAX).min(fin_corps);
let motif_drop = format!("drop({nom_variable})");
let position_drop = trouver_prochaine_occurrence(caracteres, masque, guard.position_apres_appel, zone_fin, &motif_drop);
for conflit in &reentrants {
if conflit.position_apres_appel <= guard.position_apres_appel {
continue;
}
if conflit.identifiant != guard.identifiant {
continue;
}
if conflit.position_apres_appel > zone_fin {
continue;
}
if let Some(pos_drop) = position_drop {
if pos_drop < conflit.position_apres_appel {
continue; // relâché avant le conflit potentiel
}
}
let extrait_conflit = extraire_ligne(caracteres, conflit.position_apres_appel);
if extrait_conflit.contains(MARQUEUR_EXEMPTION_LIGNE) {
continue;
}
resultats.push((
ligne_de_position(caracteres, guard.position_apres_appel),
ligne_de_position(caracteres, conflit.position_apres_appel),
guard.identifiant.clone(),
extraire_ligne(caracteres, guard.position_apres_appel),
extrait_conflit,
));
break; // un seul signalement par guard d'origine
}
}
resultats
}
/// R2 — opération lourde (ou verrou imbriqué) pendant qu'un verrou est tenu.
fn detecter_lourd_sous_verrou(caracteres: &[char], masque: &[bool], debut_corps: usize, fin_corps: usize) -> Vec<(usize, usize, String, String, String)> {
let mut resultats = Vec::new();
let verrous = recenser_appels(caracteres, masque, debut_corps, fin_corps, &[MOTIF_VERROU]);
for verrou in &verrous {
let nom_variable = match &verrou.variable_liee {
Some(n) => n,
None => continue,
};
// Convention du projet : un `.lock()` suivi de `.await` sur la même
// ligne désigne un verrou async (tokio) — hors périmètre de R2.
let ligne_verrou = extraire_ligne(caracteres, verrou.position_apres_appel);
if ligne_verrou.contains(".await") { // §18-ok — faux positif : .contains(".await") sur une String, pas un vrai await
continue;
}
let zone_fin = (verrou.position_apres_appel + LARGEUR_FENETRE_MAX).min(fin_corps);
let motif_drop = format!("drop({nom_variable})");
let position_drop = trouver_prochaine_occurrence(caracteres, masque, verrou.position_apres_appel, zone_fin, &motif_drop);
let limite_recherche = match position_drop {
Some(p) => p,
None => zone_fin,
};
// Opération lourde connue
let mut trouve: Option<(usize, String)> = None;
for motif in MOTIFS_LOURDS {
if let Some(pos) =
trouver_prochaine_occurrence(caracteres, masque, verrou.position_apres_appel, limite_recherche, motif)
{
trouve = Some((pos, format!("opération jugée lourde : {motif}")));
break;
}
}
// Verrou imbriqué sur un identifiant DIFFÉRENT
if trouve.is_none() {
for autre in &verrous {
if autre.position_apres_appel <= verrou.position_apres_appel {
continue;
}
if autre.position_apres_appel > limite_recherche {
continue;
}
if autre.identifiant == verrou.identifiant {
continue; // même verrou, pas un verrou imbriqué distinct
}
trouve = Some((autre.position_apres_appel, format!("verrou imbriqué sur `{}`", autre.identifiant)));
break;
}
}
if let Some((position_conflit, description)) = trouve {
let extrait_conflit = extraire_ligne(caracteres, position_conflit);
if extrait_conflit.contains(MARQUEUR_EXEMPTION_LIGNE) {
continue;
}
resultats.push((
ligne_de_position(caracteres, verrou.position_apres_appel),
ligne_de_position(caracteres, position_conflit),
verrou.identifiant.clone(),
extraire_ligne(caracteres, verrou.position_apres_appel),
format!("{description} — {extrait_conflit}"),
));
}
}
resultats
}
/// Fonction PURE — pour une fonction donnée, la séquence des identifiants
/// DISTINCTS verrouillés via `.lock(`, dans l'ordre de première apparition.
fn sequence_verrous_fonction(caracteres: &[char], masque: &[bool], debut_corps: usize, fin_corps: usize) -> Vec<String> {
let verrous = recenser_appels(caracteres, masque, debut_corps, fin_corps, &[MOTIF_VERROU]);
let mut ordre: Vec<String> = Vec::new();
for v in &verrous {
if !ordre.contains(&v.identifiant) {
ordre.push(v.identifiant.clone());
}
}
ordre
}
/// R3 — ordre d'acquisition incohérent entre fonctions (même paire de
/// verrous, ordre inversé). Fonction PURE sur une liste déjà agrégée de
/// séquences (une par fonction, tous fichiers confondus).
fn detecter_ordre_incoherent(sequences: &[(String, usize, Vec<String>)]) -> Vec<(String, usize, String, usize, String, String)> {
let mut registre: HashMap<(String, String), (String, usize, String)> = HashMap::new();
let mut resultats = Vec::new();
for (fichier, ligne_fn, ordre) in sequences {
for i in 0..ordre.len() {
for j in (i + 1)..ordre.len() {
let (a, b) = (&ordre[i], &ordre[j]);
if a == b {
continue;
}
let cle = if a < b { (a.clone(), b.clone()) } else { (b.clone(), a.clone()) };
let direction_ici = if a < b { "AB" } else { "BA" };
match registre.get(&cle) {
Some((direction_connue, autre_ligne, autre_fichier)) => {
if direction_connue != direction_ici {
resultats.push((
autre_fichier.clone(),
*autre_ligne,
fichier.clone(),
*ligne_fn,
cle.0.clone(),
cle.1.clone(),
));
}
}
None => {
registre.insert(cle, (direction_ici.to_string(), *ligne_fn, fichier.clone()));
}
}
}
}
}
resultats
}
fn scanner_fichier(chemin: &Path) -> Result<(Vec<Trouvaille>, Vec<(String, usize, Vec<String>)>), Erreur> {
let contenu = match fs::read_to_string(chemin) {
Ok(c) => c,
Err(e) => {
return Err(Erreur::LectureFichier {
chemin: chemin.display().to_string(),
source: e,
});
}
};
let fichier_str = chemin.display().to_string();
let caracteres: Vec<char> = contenu.chars().collect();
let masque = construire_masque_code(&caracteres);
let fonctions = extraire_fonctions(&caracteres, &masque);
let mut trouvailles = Vec::new();
let mut sequences = Vec::new();
for (ligne_fn, debut_corps, fin_corps) in fonctions {
for (ligne_o, ligne_c, ident, extrait_o, extrait_c) in
detecter_reentrance_carte(&caracteres, &masque, debut_corps, fin_corps)
{
trouvailles.push(Trouvaille {
regle: "R1-REENTRANT-GUARD",
fichier: fichier_str.clone(),
ligne_origine: ligne_o,
ligne_conflit: ligne_c,
identifiant: ident,
extrait_origine: extrait_o,
extrait_conflit: extrait_c,
});
}
for (ligne_o, ligne_c, ident, extrait_o, extrait_c) in
detecter_lourd_sous_verrou(&caracteres, &masque, debut_corps, fin_corps)
{
trouvailles.push(Trouvaille {
regle: "R2-HEAVY-UNDER-LOCK",
fichier: fichier_str.clone(),
ligne_origine: ligne_o,
ligne_conflit: ligne_c,
identifiant: ident,
extrait_origine: extrait_o,
extrait_conflit: extrait_c,
});
}
let ordre = sequence_verrous_fonction(&caracteres, &masque, debut_corps, fin_corps);
if ordre.len() >= 2 {
sequences.push((fichier_str.clone(), ligne_fn, ordre));
}
}
Ok((trouvailles, sequences))
}
fn iter_fichiers_rust(racine: &Path, resultats: &mut Vec<PathBuf>) {
let entrees = match fs::read_dir(racine) {
Ok(e) => e,
Err(e) => {
eprintln!("Avertissement : lecture dossier {} echouee : {e}", racine.display());
return;
}
};
for entree_res in entrees {
let entree = match entree_res {
Ok(e) => e,
Err(e) => {
eprintln!("Avertissement : entree de dossier illisible : {e}");
continue;
}
};
let chemin = entree.path();
let nom = match chemin.file_name() {
Some(n) => match n.to_str() {
Some(s) => s,
None => continue,
},
None => continue,
};
if DOSSIERS_IGNORES.contains(&nom) {
continue;
}
let type_fichier = match entree.file_type() {
Ok(ft) => ft,
Err(e) => {
eprintln!("Avertissement : type de {} inconnu : {e}", chemin.display());
continue; // §27-ok — intentionnel : ce fichier est ignoré, le parcours des autres continue
}
};
let est_rs = match chemin.extension() {
Some(ext) => match ext.to_str() {
Some(s) => s == "rs",
None => false, // §5-ok — extension non UTF-8 valide : jamais un fichier .rs légitime
},
None => false, // §5-ok — pas d'extension du tout : jamais un fichier .rs
};
if type_fichier.is_dir() {
iter_fichiers_rust(&chemin, resultats);
} else if est_rs {
resultats.push(chemin);
}
}
}
fn generer_rapport(
trouvailles: &[Trouvaille], incoherences: &[(String, usize, String, usize, String, String)], nb_fichiers: usize,
) -> String {
let mut rapport = String::new();
rapport.push_str("# Rapport detecteur-lock-order\n\n");
rapport.push_str(&format!(
"{nb_fichiers} fichier(s) .rs examine(s) — {} signalement(s) R1/R2, {} incoherence(s) R3.\n\n",
trouvailles.len(),
incoherences.len()
));
if trouvailles.is_empty() && incoherences.is_empty() {
rapport.push_str("Aucun risque d'interblocage detecte sur les motifs surveilles.\n");
return rapport;
}
for t in trouvailles {
rapport.push_str(&format!(
"- **{}** sur `{}` — {}:{} (origine) -> {}:{} (conflit)\n - Origine : `{}`\n - Conflit : `{}`\n",
t.regle, t.identifiant, t.fichier, t.ligne_origine, t.fichier, t.ligne_conflit,
t.extrait_origine, t.extrait_conflit
));
}
for (fichier_a, ligne_a, fichier_b, ligne_b, verrou_1, verrou_2) in incoherences {
rapport.push_str(&format!(
"- **R3-LOCK-ORDER-INCONSISTENT** entre `{verrou_1}`/`{verrou_2}` — {fichier_a}:{ligne_a} vs {fichier_b}:{ligne_b}\n"
));
}
rapport
}
fn executer(dossier_cible: &str) -> Result<String, Erreur> {
let racine = Path::new(dossier_cible);
if !racine.is_dir() {
return Err(Erreur::DossierInvalide(dossier_cible.to_string()));
}
let mut fichiers = Vec::new();
iter_fichiers_rust(racine, &mut fichiers);
let mut toutes_trouvailles = Vec::new();
let mut toutes_sequences = Vec::new();
for chemin in &fichiers {
match scanner_fichier(chemin) {
Ok((mut t, mut s)) => {
toutes_trouvailles.append(&mut t);
toutes_sequences.append(&mut s);
}
Err(e) => {
eprintln!("Avertissement : {e}");
// §27-ok — intentionnel : un fichier illisible est ignoré, le scan des autres continue
}
}
}
let incoherences = detecter_ordre_incoherent(&toutes_sequences);
Ok(generer_rapport(&toutes_trouvailles, &incoherences, fichiers.len()))
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let dossier_cible = match args.get(1) {
Some(d) => d.clone(),
None => {
eprintln!("Erreur : {}", Erreur::ArgumentManquant);
eprintln!("Usage : detecteur-lock-order <dossier_cible>");
std::process::exit(1);
}
};
match executer(&dossier_cible) {
Ok(rapport) => println!("{rapport}"),
Err(e) => {
eprintln!("Erreur : {e}");
std::process::exit(1); // §27-ok — std::process::exit diverge, aucune exécution ne suit
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn scanner_source(source: &str) -> (Vec<Trouvaille>, Vec<(String, usize, Vec<String>)>) {
let caracteres: Vec<char> = source.chars().collect();
let masque = construire_masque_code(&caracteres);
let fonctions = extraire_fonctions(&caracteres, &masque);
let mut trouvailles = Vec::new();
let mut sequences = Vec::new();
for (ligne_fn, debut_corps, fin_corps) in fonctions {
for (ligne_o, ligne_c, ident, extrait_o, extrait_c) in
detecter_reentrance_carte(&caracteres, &masque, debut_corps, fin_corps)
{
trouvailles.push(Trouvaille {
regle: "R1-REENTRANT-GUARD",
fichier: "test.rs".to_string(),
ligne_origine: ligne_o,
ligne_conflit: ligne_c,
identifiant: ident,
extrait_origine: extrait_o,
extrait_conflit: extrait_c,
});
}
for (ligne_o, ligne_c, ident, extrait_o, extrait_c) in
detecter_lourd_sous_verrou(&caracteres, &masque, debut_corps, fin_corps)
{
trouvailles.push(Trouvaille {
regle: "R2-HEAVY-UNDER-LOCK",
fichier: "test.rs".to_string(),
ligne_origine: ligne_o,
ligne_conflit: ligne_c,
identifiant: ident,
extrait_origine: extrait_o,
extrait_conflit: extrait_c,
});
}
let ordre = sequence_verrous_fonction(&caracteres, &masque, debut_corps, fin_corps);
if ordre.len() >= 2 {
sequences.push(("test.rs".to_string(), ligne_fn, ordre));
}
}
(trouvailles, sequences)
}
// ---- R1 : réentrance carte ----
#[test]
fn t_r1_detecte_get_puis_insert_sans_drop() {
let source = "fn f(carte: &Carte) {\n let g = carte.get(&k);\n carte.insert(k, v);\n}\n";
let (t, _) = scanner_source(source);
assert_eq!(t.len(), 1);
assert_eq!(t[0].regle, "R1-REENTRANT-GUARD");
assert_eq!(t[0].identifiant, "carte");
}
#[test]
fn t_r1_if_let_some_puis_get_mut_sans_drop() {
let source = "fn f(carte: &Carte) {\n if let Some(g) = carte.get(&k) {\n carte.get_mut(&k);\n }\n}\n";
let (t, _) = scanner_source(source);
assert_eq!(t.len(), 1);
}
#[test]
fn t_r1_ignore_si_drop_avant_conflit() {
let source = "fn f(carte: &Carte) {\n let g = carte.get(&k);\n let val = g.clone();\n drop(g);\n carte.insert(k, val);\n}\n";
let (t, _) = scanner_source(source);
assert!(t.is_empty(), "drop(g) avant le second appel : aucun risque");
}
#[test]
fn t_r1_ignore_carte_differente() {
let source = "fn f(a: &Carte, b: &Carte) {\n let g = a.get(&k);\n b.insert(k, v);\n}\n";
let (t, _) = scanner_source(source);
assert!(t.is_empty(), "carte b differente de a : aucune correlation attendue");
}
#[test]
fn t_r1_ignore_usage_en_ligne_sans_binding() {
let source = "fn f(carte: &Carte) {\n if carte.get(&k).is_some() {\n carte.insert(k, v);\n }\n}\n";
let (t, _) = scanner_source(source);
assert!(t.is_empty(), "aucun guard persistant lie a une variable");
}
#[test]
fn t_r1_ignore_marqueur_exemption() {
let source = "fn f(carte: &Carte) {\n let g = carte.get(&k);\n carte.insert(k, v); // lock-order-ok\n}\n";
let (t, _) = scanner_source(source);
assert!(t.is_empty());
}
#[test]
fn t_r1_ignore_chaine_brute() {
let source = "fn f() {\n let doc = r#\"let g = carte.get(&k); carte.insert(k, v);\"#;\n let _ = doc;\n}\n";
let (t, _) = scanner_source(source);
assert!(t.is_empty());
}
#[test]
fn t_r1_ignore_fonctions_differentes() {
let source = "fn a(carte: &Carte) {\n let g = carte.get(&k);\n}\nfn b(carte: &Carte) {\n carte.insert(k, v);\n}\n";
let (t, _) = scanner_source(source);
assert!(t.is_empty(), "guard et conflit dans 2 fonctions distinctes");
}
#[test]
fn t_r1_let_mut_reconnu() {
let source = "fn f(carte: &Carte) {\n let mut g = carte.get_mut(&k);\n carte.remove(&k);\n}\n";
let (t, _) = scanner_source(source);
assert_eq!(t.len(), 1);
}
// ---- R2 : opération lourde / verrou imbriqué sous verrou ----
#[test]
fn t_r2_detecte_fs_sous_verrou() {
let source = "fn f(m: &Mutex<T>) {\n let g = m.lock();\n std::fs::write(\"x\", \"y\");\n}\n"; // §31b-ok — fixture de test (chaine litterale), aucune fn reelle
let (t, _) = scanner_source(source);
assert_eq!(t.len(), 1);
assert_eq!(t[0].regle, "R2-HEAVY-UNDER-LOCK");
}
#[test]
fn t_r2_detecte_verrou_imbrique() {
let source = "fn f(a: &Mutex<T>, b: &Mutex<T>) {\n let g1 = a.lock();\n let g2 = b.lock();\n}\n"; // §31b-ok — fixture de test (chaine litterale), aucune fn reelle
let (t, _) = scanner_source(source);
assert_eq!(t.len(), 1);
assert!(t[0].extrait_conflit.contains("imbrique") || t[0].extrait_conflit.contains("b"));
}
#[test]
fn t_r2_ignore_meme_verrou_relock() {
let source = "fn f(a: &Mutex<T>) {\n let g1 = a.lock();\n let g2 = a.lock();\n}\n"; // §31b-ok — fixture de test (chaine litterale), aucune fn reelle
let (t, _) = scanner_source(source);
assert!(t.is_empty(), "meme identifiant : pas un verrou imbrique distinct");
}
#[test]
fn t_r2_ignore_si_drop_avant_operation_lourde() {
let source = "fn f(m: &Mutex<T>) {\n let g = m.lock();\n let v = 1;\n drop(g);\n std::fs::write(\"x\", \"y\");\n}\n"; // §31b-ok — fixture de test (chaine litterale), aucune fn reelle
let (t, _) = scanner_source(source);
assert!(t.is_empty());
}
#[test]
fn t_r2_ignore_lock_tokio_avec_await_meme_ligne() {
let source = "fn f(m: &TokioMutex<T>) {\n let g = m.lock().await;\n std::fs::write(\"x\", \"y\");\n}\n"; // §18-ok §31b-ok — fixture de test (chaine litterale), aucune fn/await reelle
let (t, _) = scanner_source(source);
assert!(t.is_empty(), "verrou async (tokio) hors perimetre R2 par convention");
}
#[test]
fn t_r2_ignore_operation_legere_seule() {
let source = "fn f(m: &Mutex<T>) {\n let g = m.lock();\n let v = g.champ + 1;\n}\n"; // §31b-ok — fixture de test (chaine litterale), aucune fn reelle
let (t, _) = scanner_source(source);
assert!(t.is_empty());
}
#[test]
fn t_r2_ignore_marqueur_exemption() {
let source = "fn f(m: &Mutex<T>) {\n let g = m.lock();\n std::fs::write(\"x\", \"y\"); // lock-order-ok\n}\n"; // §31b-ok — fixture de test (chaine litterale), aucune fn reelle
let (t, _) = scanner_source(source);
assert!(t.is_empty());
}
// ---- R3 : ordre incohérent entre fonctions ----
#[test]
fn t_r3_detecte_ordre_inverse() {
let source = "fn f1(a: &Mutex<T>, b: &Mutex<T>) {\n let g1 = a.lock();\n let g2 = b.lock();\n}\nfn f2(a: &Mutex<T>, b: &Mutex<T>) {\n let g1 = b.lock();\n let g2 = a.lock();\n}\n"; // §31b-ok — fixture de test (chaine litterale), aucune fn reelle
let (_, sequences) = scanner_source(source);
let incoherences = detecter_ordre_incoherent(&sequences);
assert_eq!(incoherences.len(), 1);
}
#[test]
fn t_r3_ignore_meme_ordre() {
let source = "fn f1(a: &Mutex<T>, b: &Mutex<T>) {\n let g1 = a.lock();\n let g2 = b.lock();\n}\nfn f2(a: &Mutex<T>, b: &Mutex<T>) {\n let g1 = a.lock();\n let g2 = b.lock();\n}\n"; // §31b-ok — fixture de test (chaine litterale), aucune fn reelle
let (_, sequences) = scanner_source(source);
let incoherences = detecter_ordre_incoherent(&sequences);
assert!(incoherences.is_empty(), "meme ordre dans les 2 fonctions : aucune incoherence");
}
#[test]
fn t_r3_ignore_fonction_avec_un_seul_verrou() {
let source = "fn f1(a: &Mutex<T>) {\n let g1 = a.lock();\n}\n"; // §31b-ok — fixture de test (chaine litterale), aucune fn reelle
let (_, sequences) = scanner_source(source);
assert!(sequences.is_empty(), "moins de 2 verrous distincts : rien a comparer");
}
#[test]
fn t_r3_ignore_verrous_disjoints() {
let source = "fn f1(a: &Mutex<T>, b: &Mutex<T>) {\n let g1 = a.lock();\n let g2 = b.lock();\n}\nfn f2(c: &Mutex<T>, d: &Mutex<T>) {\n let g1 = d.lock();\n let g2 = c.lock();\n}\n"; // §31b-ok — fixture de test (chaine litterale), aucune fn reelle
let (_, sequences) = scanner_source(source);
let incoherences = detecter_ordre_incoherent(&sequences);
assert!(incoherences.is_empty(), "aucune paire de verrous partagee entre les 2 fonctions");
}
// ---- rapport ----
#[test]
fn t_generer_rapport_vide() {
let rapport = generer_rapport(&[], &[], 4);
assert!(rapport.contains("4 fichier"));
assert!(rapport.contains("Aucun risque"));
}
#[test]
fn t_generer_rapport_avec_trouvailles() {
let trouvailles = vec![Trouvaille {
regle: "R1-REENTRANT-GUARD",
fichier: "exemple.rs".to_string(),
ligne_origine: 3,
ligne_conflit: 5,
identifiant: "carte".to_string(),
extrait_origine: "let g = carte.get(&k);".to_string(),
extrait_conflit: "carte.insert(k, v);".to_string(),
}];
let rapport = generer_rapport(&trouvailles, &[], 1);
assert!(rapport.contains("exemple.rs:3"));
assert!(rapport.contains("R1-REENTRANT-GUARD"));
}
}