diff --git a/COMPTE_RENDU.md b/COMPTE_RENDU.md new file mode 100644 index 0000000..5e34e8e --- /dev/null +++ b/COMPTE_RENDU.md @@ -0,0 +1,552 @@ +# Compte rendu — TD2 : complexité cyclomatique + +**R5.A.04 · Qualité Algorithmique** + +**Groupe :** Ryad MEHIRI · Luka PLOUVIER · Jimmy COLOMB-RAVAT + +Fichiers analysés : [`exercices.c`](exercices.c) (C1 à C6), [`Bibliotheque.java`](Bibliotheque.java) (J1 à J6) et [`exercices.py`](exercices.py) (P1 à P5). Comme demandé, les fonctions `main` et les constructeurs ne sont pas analysés. + +--- + +## Synthèse + +| Ex. | Fonction | Décisions | **M** | Classe | +|-----|----------|:---------:|:-----:|--------| +| C1 | `carre` | 0 | **1** | Simple | +| C2 | `moyenne` | 2 | **3** | Simple | +| C3 | `est_bissextile` | 2 | **3** | Simple | +| C4 | `nom_jour` | 7 | **8** | Simple | +| C5 | `compter_mots` | 7 | **8** | Simple | +| C6 | `date_valide` | 10 | **11** | Modéré | +| J1 | `nombreDeLivres` | 0 | **1** | Simple | +| J2 | `estDisponible` | 2 | **3** | Simple | +| J3 | `peutEmprunter` | 8 | **9** | Simple | +| J4 | `calculerPenalite` | 8 | **9** | Simple | +| J5 | `rechercher` | 10 | **11** | Modéré | +| J6 | `importer` | 7 | **8** | Simple | +| P1 | `celsius_vers_fahrenheit` | 0 | **1** | Simple | +| P2 | `signe` | 2 | **3** | Simple | +| P3 | `notes_valides` | 3 | **4** | Simple | +| P4 | `mention` | 2 | **3** | Simple | +| P5 | `analyser_mot_de_passe` | 16 | **17** | Modéré | + +La fonction la plus complexe est **P5 `analyser_mot_de_passe` (M = 17)**. Nous l'avons découpée en 7 fonctions qui ont toutes M ≤ 4 (voir la [partie 6](#6-refactorisation-de-p5-analyser_mot_de_passe)). + +Pour P3 et P5, nous comptons la comparaison chaînée de Python (`0 <= n <= 20`) comme une décision. Un comptage par mots-clés ne la voit pas et donne 3 et 16 (voir la [partie 5](#5-cas-particuliers)). + +--- + +## 1. Méthode + +Nous avons utilisé deux méthodes et vérifié qu'elles donnent le même résultat : + +- **Graphe de flot de contrôle :** `M = E − N + 2P`, avec E le nombre d'arêtes, N le nombre de nœuds et P le nombre de composantes connexes. On a **P = 1** pour une fonction, donc `M = E − N + 2`. +- **Comptage des décisions :** `M = décisions + 1`. + +### Ce qui compte comme une décision + +| Ajoute **+1** | Ajoute **0** | +|---|---| +| `if`, `else if` / `elif` | `else`, `default` | +| `for`, for-each, `while`, `do … while` | `return`, `break`, `continue` | +| chaque `case` | `try`, `finally` | +| chaque `catch` / `except` | appel d'une autre fonction (ses décisions comptent dans son propre M) | +| chaque `&&`, `\|\|`, `and`, `or` | négation `!` / `not` | +| ternaire `a ? b : c` / `b if a else c` | | +| `for` et `if` d'une compréhension Python | | +| comparaison chaînée Python `a <= b <= c` (un `and` implicite) | | + +### Règles de construction des graphes (diapo 31 du cours) + +1. Un **nœud** est un bloc d'instructions sans branchement. Les instructions qui précèdent une condition sont regroupées avec elle. +2. Un nœud de **décision** a deux sorties (une sortie par `case` pour un `switch`). Une boucle ajoute une arête de retour. +3. Tous les `return` mènent à **un seul nœud de fin**. +4. Chaque `&&` / `||` est une décision séparée, à cause de l'évaluation paresseuse. + +--- + +## 2. Graphes de flot de contrôle (C2, C4, J2, J6, P2, P3) + +Nous avons dessiné les six graphes à la main : + +![Graphes de flot de contrôle de C2, C4, J2, J6, P2 et P3, dessinés à la main](graphes/graphes_a_la_main.webp) + +Les calculs ne sont pas sur la feuille : ils sont détaillés ci-dessous, graphe par graphe. Chaque sous-partie donne : + +- le **code**, avec à droite de chaque ligne le numéro de son nœud sur le dessin ; +- la liste des **arêtes**, avec entre parenthèses la condition qui fait suivre chaque arête sortant d'une décision ; +- le **calcul** `M = E − N + 2P` (avec P = 1), puis la **vérification** par le nombre de décisions. + +### Récapitulatif + +| Graphe | Fonction | N (nœuds) | E (arêtes) | M = E − N + 2 | Vérification : décisions + 1 | +|:---:|---|:---:|:---:|:---:|:---:| +| C2 | `moyenne` | 7 | 8 | **3** | 2 + 1 = 3 ✔ | +| C4 | `nom_jour` | 10 | 16 | **8** | 7 + 1 = 8 ✔ | +| J2 | `estDisponible` | 4 | 5 | **3** | 2 + 1 = 3 ✔ | +| J6 | `importer` | 12 | 18 | **8** | 7 + 1 = 8 ✔ | +| P2 | `signe` | 6 | 7 | **3** | 2 + 1 = 3 ✔ | +| P3 | `notes_valides` | 6 | 8 | **4** | 3 + 1 = 4 ✔ | + +### C2 — `moyenne` + +```c +int moyenne(const int *t, int n) +{ + int somme = 0; // 1 + for (int i = 0; i < n; i++) // 1 (i = 0) · 2 (i < n) · 3 (i++) + somme += t[i]; // 3 + return n > 0 ? somme / n : 0; // 4 (n > 0) · 5 (somme / n) · 6 (0) · 7 (return) +} +``` + +- **Nœuds (7) :** 1 à 7. Les nœuds de décision sont 2 (`i < n`) et 4 (`n > 0`). +- **Arêtes (8) :** 1→2, 2→3 (`i < n`), 3→2 (retour de boucle), 2→4 (sinon), 4→5 (`n > 0`), 4→6 (sinon), 5→7, 6→7 +- **Calcul :** M = E − N + 2P = 8 − 7 + 2 × 1 = **3** +- **Vérification :** 2 décisions (`for`, `?:`) + 1 = **3** ✔ + +> **Remarque :** comme dans l'exemple `signe_somme` du cours, un des 3 chemins de base est impossible à l'exécution. +> - Chemin 1 : 1→2→4→6→7, testé avec `n = 0` (résultat 0). +> - Chemin 2 : 1→2→3→2→4→5→7, testé avec `t = {12}, n = 1` (résultat 12). +> - Chemin 3 : 1→2→4→5→7, impossible : si on n'entre pas dans la boucle, alors `n ≤ 0` et `n > 0` est forcément faux. +> +> Les deux décisions dépendent l'une de l'autre, ce que le graphe ne voit pas. Deux tests suffisent donc à couvrir toutes les arêtes. + +### C4 — `nom_jour` + +```c +const char *nom_jour(int jour) +{ + switch (jour) { // 1 + case 1: return "lundi"; // 2 + case 2: return "mardi"; // 3 + case 3: return "mercredi"; // 4 + case 4: return "jeudi"; // 5 + case 5: return "vendredi"; // 6 + case 6: return "samedi"; // 7 + case 7: return "dimanche"; // 8 + default: return "inconnu"; // 9 + } +} // 10 (fin) +``` + +- **Nœuds (10) :** 1 à 10. Le seul nœud de décision est 1 (le `switch`), qui a 8 sorties. +- **Arêtes (16) :** 1→2 (`case 1`), 1→3 (`case 2`), 1→4 (`case 3`), 1→5 (`case 4`), 1→6 (`case 5`), 1→7 (`case 6`), 1→8 (`case 7`), 1→9 (`default`), puis 2→10, 3→10, 4→10, 5→10, 6→10, 7→10, 8→10, 9→10 +- **Calcul :** M = E − N + 2P = 16 − 10 + 2 × 1 = **8** +- **Vérification :** 7 `case` + 1 = **8** ✔. Le `default` est le « sinon » du `switch` et ne compte pas. + +### J2 — `estDisponible` + +```java +return !livre.emprunte // 1 + && !livre.reserve // 2 + && !livre.abime; // 3 (valeur renvoyée) · 4 (fin) +``` + +Il n'y a pas de `if`, mais chaque `&&` crée un branchement : si `!livre.emprunte` est faux, la suite n'est pas évaluée et la méthode renvoie directement `false`. On construit le graphe comme celui de `est_bissextile` dans le cours. + +- **Nœuds (4) :** 1 à 4. Les nœuds de décision sont 1 (`!livre.emprunte`) et 2 (`!livre.reserve`). +- **Arêtes (5) :** 1→2 (`!emprunte` vrai), 1→4 (sinon : renvoie `false`), 2→3 (`!reserve` vrai), 2→4 (sinon : renvoie `false`), 3→4 (renvoie `!abime`) +- **Calcul :** M = E − N + 2P = 5 − 4 + 2 × 1 = **3** +- **Vérification :** 2 `&&` + 1 = **3** ✔ + +### J6 — `importer` + +```java +int importes = 0; // 1 +for (String ligne : lignes) { // 2 + if (ligne.isBlank() || ligne.startsWith("#")) // 3 (isBlank) · 4 (startsWith) + continue; + String[] champs = ligne.split(";"); // 5 + if (champs.length != 4) { // 5 + System.err.println("Ligne ignorée : " + ligne); // 6 + continue; + } + try { + int annee = Integer.parseInt(champs[3].trim()); // 7 (peut lever l'exception) + if (annee > 0 && annee <= 2026) { // 8 (annee > 0) · 9 (annee <= 2026) + livres.add(new Livre(...)); // 10 + importes++; // 10 + } + } catch (NumberFormatException e) { + System.err.println("Année invalide : " + champs[3]); // 11 + } +} +return importes; // 12 +``` + +- **Nœuds (12) :** 1 à 12. Les nœuds de décision sont 2, 3, 4, 5, 7, 8 et 9. +- **Arêtes (18) :** + - descente principale : 1→2, 2→3 (ligne suivante), 3→4 (sinon), 4→5 (sinon), 5→7 (sinon), 7→8 (pas d'exception), 8→9 (`annee > 0`), 9→10 (`annee <= 2026`) ; + - sorties sur le côté : 2→12 (plus de ligne), 5→6 (`champs.length != 4`), 7→11 (`NumberFormatException`) ; + - retours vers le nœud 2 : 3→2 (`isBlank()`, `continue`), 4→2 (`startsWith("#")`, `continue`), 6→2 (`continue`), 8→2 (sinon), 9→2 (sinon), 10→2 (fin du tour), 11→2 (fin du tour). +- **Calcul :** M = E − N + 2P = 18 − 12 + 2 × 1 = **8** +- **Vérification :** 7 décisions (for-each, `if`, `||`, `if`, `if`, `&&`, `catch`) + 1 = **8** ✔. On retrouve les 7 nœuds de décision du graphe. + +Le nœud 7 est une décision parce que `Integer.parseInt` a deux sorties : la suite normale du `try` ou le saut vers le `catch`. C'est cette arête exceptionnelle (7→11) que compte le `+1` du `catch`. + +### P2 — `signe` + +```python +def signe(x): + if x > 0: # 1 + return 1 # 2 + elif x < 0: # 3 + return -1 # 4 + return 0 # 5 (fin : 6) +``` + +- **Nœuds (6) :** 1 à 6. Les nœuds de décision sont 1 (`x > 0`) et 3 (`x < 0`). +- **Arêtes (7) :** 1→2 (`x > 0`), 1→3 (sinon), 3→4 (`x < 0`), 3→5 (sinon), 2→6, 4→6, 5→6 +- **Calcul :** M = E − N + 2P = 7 − 6 + 2 × 1 = **3** +- **Vérification :** `if` + `elif` + 1 = **3** ✔. Trois tests suffisent : `x = 5`, `x = −3` et `x = 0`. + +### P3 — `notes_valides` + +```python +def notes_valides(notes): + return [n for n in notes if 0 <= n <= 20] +``` + +La compréhension est une boucle `for` avec un filtre `if`, écrits sur une seule ligne. La condition `0 <= n <= 20` équivaut à `0 <= n and n <= 20` : si `0 <= n` est faux, Python n'évalue pas `n <= 20`. Cela fait donc deux décisions, comme pour un `and` (règle 4). + +Comme tout tient sur une ligne, voici ce que représente chaque nœud : + +| Nœud | Rôle | +|:---:|---| +| 1 | création de la liste résultat (vide) | +| 2 | `for n in notes` : reste-t-il un élément ? | +| 3 | `0 <= n` | +| 4 | `n <= 20` | +| 5 | ajout de `n` à la liste | +| 6 | `return` de la liste (fin) | + +- **Nœuds (6) :** 1 à 6. Les nœuds de décision sont 2, 3 et 4. +- **Arêtes (8) :** 1→2, 2→3 (élément suivant), 2→6 (plus d'élément), 3→4 (`0 <= n`), 3→2 (sinon), 4→5 (`n <= 20`), 4→2 (sinon), 5→2 +- **Calcul :** M = E − N + 2P = 8 − 6 + 2 × 1 = **4** +- **Vérification :** `for` + `if` + `and` implicite + 1 = **4** ✔ + +Si on ne découpe pas la comparaison chaînée (en fusionnant les nœuds 3 et 4), on obtient N = 5, E = 6 et **M = 3**. C'est la valeur que donne un comptage par mots-clés. + +--- + +## 3. Comptage des décisions (toutes les fonctions) + +### 3.1 `exercices.c` + +| Ex. | Fonction | Points de décision | Décisions | M | +|-----|----------|--------------------|:---:|:---:| +| C1 | `carre` | aucun | 0 | **1** | +| C2 | `moyenne` | `for`, `?:` | 2 | **3** | +| C3 | `est_bissextile` | `&&`, `\|\|` | 2 | **3** | +| C4 | `nom_jour` | 7 `case` (`default` ne compte pas) | 7 | **8** | +| C5 | `compter_mots` | `while`, 4 `\|\|`, `if`, `else if` | 7 | **8** | +| C6 | `date_valide` | `if` + 3 `\|\|`, `if`, `if`, `else if` + 3 `\|\|` | 10 | **11** | + +C3 donne le même graphe que l'exemple du cours (N = 4, E = 5, M = 3). Les deux opérateurs logiques sont des décisions, même sans `if`. + +**C5 `compter_mots` : M = 8** + +```c +while (texte[i] != '\0') { // +1 while + char c = texte[i]; + int separateur = (c == ' ' || c == '\t' || c == '\n' || c == ',' || c == '.'); // +4 (4 ||) + if (separateur) { // +1 if + dans_mot = 0; + } else if (!dans_mot) { // +1 else if + dans_mot = 1; + mots++; + } + i++; +} +``` + +On a 1 + 4 + 1 + 1 = 7 décisions, donc M = 8. Les 4 `||` sont dans une affectation et non dans un `if`, mais ils comptent quand même. + +**C6 `date_valide` : M = 11** + +```c +if (annee < 1 || mois < 1 || mois > 12 || jour < 1) // +1 if, +3 || + return 0; +if (mois == 2) { // +1 if + if (est_bissextile(annee)) // +1 if (l'appel lui-même : 0) + max = 29; + else // 0 + max = 28; +} else if (mois == 4 || mois == 6 || mois == 9 || mois == 11) { // +1 else if, +3 || + max = 30; +} else { // 0 + max = 31; +} +return jour <= max; // 0 : simple comparaison renvoyée +``` + +On a 4 + 1 + 1 + 4 = 10 décisions, donc M = 11. Les 3 décisions de `est_bissextile` sont comptées dans C3 et pas ici. + +### 3.2 `Bibliotheque.java` + +| Ex. | Méthode | Points de décision | Décisions | M | +|-----|---------|--------------------|:---:|:---:| +| J1 | `nombreDeLivres` | aucun | 0 | **1** | +| J2 | `estDisponible` | 2 `&&` | 2 | **3** | +| J3 | `peutEmprunter` | 4 `if`, `?:`, 2 `else if`, `&&` | 8 | **9** | +| J4 | `calculerPenalite` | `if` + `\|\|`, `if`, `else if`, `for`, `if`, `if`, `?:` | 8 | **9** | +| J5 | `rechercher` | for-each, `if` + `&&`, `if`, `if` + `&&`, `if` + `\|\|`, `if` + `\|\|` | 10 | **11** | +| J6 | `importer` | for-each, `if` + `\|\|`, `if`, `if` + `&&`, `catch` | 7 | **8** | + +**J3 `peutEmprunter` : M = 9** + +```java +if (!a.cotisationAJour) // +1 + return "Cotisation à renouveler"; +if (a.joursDeRetardCumules > 30) // +1 + return "Trop de retards"; +int maxEmprunts = a.etudiant ? 8 : 5; // +1 (ternaire) +if (a.nbEmprunts >= maxEmprunts) // +1 + return "Nombre maximum d'emprunts atteint"; +if (l.emprunte) // +1 + return "Livre déjà emprunté"; +else if (l.reserve && !a.etudiant) // +1 else if, +1 && + return "Livre réservé"; +else if (l.abime) // +1 + return "Livre en réparation"; +return "OK"; +``` + +On a 8 décisions, donc M = 9. Le ternaire est facile à oublier parce qu'il est caché dans une affectation. + +**J4 `calculerPenalite` : M = 9** + +```java +if (joursRetard <= 0 || nbLivres <= 0) // +1 if, +1 || + return 0; +if (joursRetard <= 7) // +1 + tarif = 0.20; +else if (joursRetard <= 30) // +1 + tarif = 0.50; +else // 0 + tarif = 1.00; +for (int i = 0; i < nbLivres; i++) { // +1 + penalite += joursRetard * tarif; + if (i >= 2) // +1 + penalite += 1.0; +} +if (a.etudiant) // +1 + penalite *= 0.5; +return penalite > 50 ? 50 : penalite; // +1 (ternaire) +``` + +On a 8 décisions, donc M = 9. + +**J5 `rechercher` : M = 11** + +```java +for (Livre l : livres) { // +1 for-each + if (genre != null && !l.genre.equals(genre)) // +1 if, +1 && + continue; + if (l.annee < anneeMin) // +1 + continue; + if (disponiblesSeulement && !estDisponible(l)) // +1 if, +1 && (l'appel : 0) + continue; + if (motCle == null || motCle.isEmpty()) { // +1 if, +1 || + resultats.add(l); + } else { // 0 + String m = motCle.toLowerCase(); + if (l.titre.toLowerCase().contains(m) + || l.auteur.toLowerCase().contains(m)) // +1 if, +1 || + resultats.add(l); + } +} +``` + +On a 1 + 2 + 1 + 2 + 2 + 2 = 10 décisions, donc M = 11. + +Le détail de J6 est donné avec son graphe dans la partie 2. + +### 3.3 `exercices.py` + +| Ex. | Fonction | Points de décision | Décisions | M | +|-----|----------|--------------------|:---:|:---:| +| P1 | `celsius_vers_fahrenheit` | aucun | 0 | **1** | +| P2 | `signe` | `if`, `elif` | 2 | **3** | +| P3 | `notes_valides` | `for` et `if` de la compréhension, comparaison chaînée | 3 | **4** | +| P4 | `mention` | `for`, `if` | 2 | **3** | +| P5 | `analyser_mot_de_passe` | voir le détail dans la partie 6.1 | 16 | **17** | + +**P4 `mention` :** sans le dictionnaire, il aurait fallu une cascade de 4 `if`/`elif`, soit M = 5. Le dictionnaire parcouru dans une boucle ramène M à 3. C'est la technique « remplacer les `if` en cascade par un tableau » du cours (diapo 38). Attention : la fonction dépend de l'ordre d'insertion du dictionnaire (16, 14, 12, 10), qui est garanti depuis Python 3.7. + +--- + +## 4. Classement + +Nous utilisons la grille du cours : **1–10 simple**, **11–20 modéré**, **21–50 complexe**, **> 50 très risqué**. + +| Classe | Fonctions | +|--------|-----------| +| **Simple (1–10)** | C1 (1), C2 (3), C3 (3), C4 (8), C5 (8), J1 (1), J2 (3), J3 (9), J4 (9), J6 (8), P1 (1), P2 (3), P3 (4), P4 (3) | +| **Modéré (11–20)** | C6 `date_valide` (11), J5 `rechercher` (11), P5 `analyser_mot_de_passe` (17) | +| **Complexe (21–50)** | aucune | +| **Très risqué (> 50)** | aucune | + +**Analyse :** + +- Aucune fonction ne dépasse 20. Les trois fonctions modérées dépassent le seuil de McCabe (10). Ce sont elles qu'il faudrait revoir en priorité, en commençant par **P5**, qui est loin devant les autres. +- **J3 et J4 (M = 9)** sont juste sous le seuil. Une condition de plus suffirait à les faire passer en « modéré ». +- Le tableau du sujet (diapo 46) indique la difficulté de l'exercice, pas le classement de la grille. J3, J4 et J6 y sont marqués « complexes » parce qu'ils contiennent des pièges (ternaires, `catch`, boucles), mais leur M reste ≤ 10. +- **C4 `nom_jour` (M = 8)** montre une limite de la mesure : un `switch` qui associe un numéro à un nom se lit d'un coup d'œil, mais il obtient le même M que `compter_mots`, qui est bien plus difficile à suivre. + +--- + +## 5. Cas particuliers + +| Construction | Où | Compte | Justification | +|---|---|:---:|---| +| `&&` / `\|\|` **sans `if`** | C3, J2, C5 (`separateur`) | +1 chacun | Avec l'évaluation paresseuse, le second opérande n'est évalué que dans certains cas. Il y a donc un branchement dans le graphe, même si l'expression est dans un `return` ou une affectation (voir le graphe de J2). | +| Ternaire `?:` / `b if a else c` | C2, J3, J4, P5 | +1 | C'est un `if`/`else` à l'intérieur d'une expression, avec deux sorties. | +| `switch` / `case` | C4 | +1 par `case` | Chaque `case` est une sortie du nœud de décision. Le `default` est le « sinon » et ne compte pas. Ici, les `return` remplacent les `break` et rejoignent tous le nœud de fin. Le cours note la limite : un `switch` lisible obtient un M élevé. Certaines variantes (complexité cyclomatique « modifiée », complexité cognitive) comptent le `switch` une seule fois, ce qui donnerait M = 2. | +| **Compréhension** Python | P3 (et P4, P5 pour les boucles) | +1 pour le `for`, +1 par `if` | `[n for n in notes if cond]` est une boucle avec un filtre écrite sur une ligne. Le graphe est le même que celui d'une boucle `for` qui contient un `if`. | +| **Comparaison chaînée** | P3, P5 | +1 | `a <= b <= c` équivaut à `a <= b and b <= c`. Si le premier test est faux, le second n'est pas évalué, ce qui crée une décision cachée. Nous la comptons pour rester cohérents avec la règle 4. Sans elle, on aurait P3 = 3 et P5 = 16. | +| `catch` / `except` | J6 | +1 par `catch` | Le `catch` ajoute une sortie exceptionnelle au bloc `try` (arête 7→11 dans le graphe de J6). Le `try` ne compte pas car il n'y a pas de choix à l'entrée. Un `finally` ne compterait pas non plus car il est toujours exécuté. | +| for-each | J5, J6, P4, P5 | +1 | À chaque tour, on teste s'il reste un élément, comme dans une boucle `for`. | +| `break`, `continue`, `return` | J5, J6, P5, … | 0 | Ce sont des sauts sans choix. La décision est dans le `if` qui les précède. | +| `else`, `default` | C6, J4, J5, P5 | 0 | C'est la sortie « sinon » d'une décision déjà comptée. | +| Appel d'une fonction | C6 (`est_bissextile`), J5 (`estDisponible`) | 0 | M se calcule fonction par fonction. Les décisions de la fonction appelée sont dans son propre M. | +| Négation `!` / `not` | J2, J3, J5, P5 | 0 | Elle inverse une valeur sans créer de branchement. | +| Dictionnaire de seuils | P4 | — | Les données remplacent des décisions. La logique reste la même, mais M passe de 5 à 3. | + +--- + +## 6. Refactorisation de P5 `analyser_mot_de_passe` + +### 6.1 Version d'origine : M = 17 + +```python +def analyser_mot_de_passe(mdp, login=None): + problemes = [] + if len(mdp) < 12: # +1 + problemes.append("trop court") + majuscule = minuscule = chiffre = special = False + for c in mdp: # +1 + if c.isupper(): # +1 + majuscule = True + elif c.islower(): # +1 + minuscule = True + elif c.isdigit(): # +1 + chiffre = True + else: # 0 + special = True + if not (majuscule and minuscule): # +1 if, +1 and + problemes.append("mélanger majuscules et minuscules") + if not chiffre: # +1 + problemes.append("ajouter un chiffre") + if not special: # +1 + problemes.append("ajouter un caractère spécial") + if login and login.lower() in mdp.lower(): # +1 if, +1 and + problemes.append("ne pas contenir le login") + for i in range(len(mdp) - 2): # +1 + if mdp[i] == mdp[i + 1] == mdp[i + 2]: # +1 if, +1 comparaison chaînée + problemes.append("éviter 3 caractères identiques") + break # 0 + if not problemes: # +1 + return "fort", [] + return ("moyen" if len(problemes) <= 2 else "faible"), problemes # +1 (ternaire) +``` + +On a 16 décisions, donc **M = 17**. La fonction est classée **modérée** et dépasse largement le seuil de 10. + +**Ce qui pose problème :** + +- La fonction fait quatre choses à la fois : classer les caractères, vérifier six règles, détecter les répétitions et calculer le niveau. +- Les quatre drapeaux booléens (`majuscule`, `minuscule`, `chiffre`, `special`) relient le début et la fin de la fonction. Pour comprendre un message, il faut suivre tout le code. +- Les six règles sont écrites comme une suite de `if` qui ajoutent chacun un message. C'est le cas typique de « `if` en cascade ». +- Les valeurs `12` et `2` sont des nombres magiques. + +### 6.2 Techniques appliquées (diapo 38 du cours) + +1. **Extraire des fonctions** pour donner une seule responsabilité à chacune : `_categorie`, `_categories`, `_contient_login`, `_trois_identiques`, `_lister_problemes`, `_niveau`. +2. **Remplacer les `if` en cascade par un tableau** : les six règles deviennent un tuple de paires `(échec ?, message)`, filtré par une seule compréhension. +3. **Remplacer les drapeaux par un ensemble** : `_categories(mdp)` renvoie l'ensemble des catégories présentes, par exemple `{"majuscule", "chiffre"}`. +4. **Supprimer la décision cachée** : `mdp[i:i + 3] == mdp[i] * 3` remplace la comparaison chaînée par une seule comparaison. +5. **Nommer les constantes** : `LONGUEUR_MIN = 12` et `MAX_PROBLEMES_MOYEN = 2`. + +### 6.3 Nouvelle version ([`exercices_refactorise.py`](exercices_refactorise.py)) + +```python +LONGUEUR_MIN = 12 +MAX_PROBLEMES_MOYEN = 2 + + +def _categorie(c): # M = 4 + if c.isupper(): + return "majuscule" + if c.islower(): + return "minuscule" + if c.isdigit(): + return "chiffre" + return "special" + + +def _categories(mdp): # M = 2 + return {_categorie(c) for c in mdp} + + +def _contient_login(mdp, login): # M = 2 + return bool(login) and login.lower() in mdp.lower() + + +def _trois_identiques(mdp): # M = 2 + return any(mdp[i:i + 3] == mdp[i] * 3 for i in range(len(mdp) - 2)) + + +def _lister_problemes(mdp, login): # M = 3 + presentes = _categories(mdp) + controles = ( + (len(mdp) < LONGUEUR_MIN, "trop court"), + (not {"majuscule", "minuscule"}.issubset(presentes), "mélanger majuscules et minuscules"), + ("chiffre" not in presentes, "ajouter un chiffre"), + ("special" not in presentes, "ajouter un caractère spécial"), + (_contient_login(mdp, login), "ne pas contenir le login"), + (_trois_identiques(mdp), "éviter 3 caractères identiques"), + ) + return [message for echec, message in controles if echec] + + +def _niveau(problemes): # M = 3 + if not problemes: + return "fort" + return "moyen" if len(problemes) <= MAX_PROBLEMES_MOYEN else "faible" + + +def analyser_mot_de_passe(mdp, login=None): # M = 1 + problemes = _lister_problemes(mdp, login) + return _niveau(problemes), problemes +``` + +### 6.4 Nouveau calcul de M + +| Fonction | Points de décision | Décisions | M | Classe | +|----------|--------------------|:---:|:---:|---| +| `_categorie` | `if`, `if`, `if` | 3 | **4** | Simple | +| `_categories` | `for` de la compréhension | 1 | **2** | Simple | +| `_contient_login` | `and` | 1 | **2** | Simple | +| `_trois_identiques` | `for` du générateur (une seule comparaison, plus de chaînage) | 1 | **2** | Simple | +| `_lister_problemes` | `for` et `if` de la compréhension | 2 | **3** | Simple | +| `_niveau` | `if`, ternaire | 2 | **3** | Simple | +| `analyser_mot_de_passe` | aucun | 0 | **1** | Simple | + +**Bilan :** + +| | Avant | Après | +|---|:---:|:---:| +| M de `analyser_mot_de_passe` | **17** (modéré) | **1** (simple) | +| M maximal d'une fonction | 17 | **4** | +| Somme des M | 17 | 17 (7 fonctions) | + +- **La complexité n'a pas disparu, elle est répartie.** La somme des M reste 17 : il faut toujours vérifier les mêmes cas. Mais chaque fonction a au plus 4 chemins et se teste seule, avec 4 tests au maximum. `analyser_mot_de_passe` n'a plus qu'un chemin et se lit comme sa spécification : « lister les problèmes, puis en déduire le niveau ». +- **Ajouter une règle ne change plus M** : il suffit d'ajouter une ligne dans `controles`. +- Le résultat de la fonction ne change pas : mêmes messages, dans le même ordre, mêmes niveaux et même type de retour (un tuple `(niveau, liste)`). La commande `python exercices_refactorise.py` compare l'ancienne et la nouvelle version sur 5 011 mots de passe (11 cas choisis à la main et 5 000 chaînes aléatoires) avec 5 logins différents (`None`, `""`, …), soit 25 055 comparaisons. + +### 6.5 Pistes pour les deux autres fonctions modérées + +- **C6 `date_valide` (11) :** mettre le nombre de jours de chaque mois dans un tableau `JOURS_PAR_MOIS[12]` et extraire une fonction `jours_dans_mois(mois, annee)`, qui ne garde que le cas de février (`&&` et ternaire, M = 3). `date_valide` ne garde alors que la validation des bornes, et la cascade `mois == 4 || mois == 6 || …` disparaît. +- **J5 `rechercher` (11) :** extraire deux méthodes, `respecteFiltres(l, genre, anneeMin, disponiblesSeulement)` et `contientMotCle(l, motCle)`. La boucle n'aurait plus qu'un `if`. diff --git a/graphes/graphes_a_la_main.webp b/graphes/graphes_a_la_main.webp new file mode 100644 index 0000000..2b00556 Binary files /dev/null and b/graphes/graphes_a_la_main.webp differ