📄 main.rs 🔒 b53b85ce…f31aa1f2 Se connecter pour télécharger ← Retour
// ⛔ 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"));
    }
}
39.2 Ko BLAKE3 : b53b85ce…f31aa1f2