TD2 : compte rendu complexité cyclomatique + refactorisation de P5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 10:20:46 +02:00
parent cecf7379f6
commit e6138e8f5d
8 changed files with 347 additions and 0 deletions
+295
View File
@@ -0,0 +1,295 @@
Derqsi bilal
Pierre Courtehoux
Youness Boulalam
TD2 - Complexité cyclomatique
Règles de comptage utilisées
On utilise M = décisions + 1, en comptant +1 pour chaque if, else if / elif, for, while, case, catch / except, && / || / and / or et ternaire (a ? b : c ou x if c else y).
else, default, return, break, continue et finally ne comptent pas.
Grille du cours : 1 – 10 simple, 11 – 20 modéré, 21 – 50 complexe, > 50 très risqué.
1. Graphes de flot de contrôle
Même présentation que dans le cours : les numéros dans le code correspondent aux nœuds, en orange les décisions, en gris foncé les blocs.
Pour chaque graphe : N = nombre de nœuds, E = nombre d'arêtes, P = 1 (une seule fonction), et M = E − N + 2P.
C2 - moyenne
```c
int somme = 0; // 1
for (int i = 0; i < n; i++) // 2 (test i < n)
somme += t[i]; // 3
return n > 0 ? somme / n : 0; // 4 (test n > 0), 5 (somme / n), 6 (0)
// 7 fin
```
![Graphe C2](images/c2_moyenne.png)
N = 7 · E = 8 → M = 8 − 7 + 2 = 3
C4 - nom_jour
Le switch est un nœud qui a une sortie par case + une pour default.
```c
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
```
![Graphe C4](images/c4_nom_jour.png)
N = 10 (switch + 8 return + fin) · E = 16 (8 sorties du switch + 8 vers la fin) → M = 16 − 10 + 2 = 8
J2 - estDisponible
Il n'y a pas de if, mais && est paresseux : si !livre.emprunte est faux, la suite n'est pas évaluée. Chaque && crée donc un branchement (même principe que est_bissextile dans le cours).
```java
return !livre.emprunte // A
&& !livre.reserve // B
&& !livre.abime; // C (renvoyé)
// F fin
```
![Graphe J2](images/j2_estDisponible.png)
N = 4 · E = 5 → M = 5 − 4 + 2 = 3
J6 - importer
```java
int importes = 0; // 1
for (String ligne : lignes) { // 2 (encore une ligne ?)
if (ligne.isBlank() // 3
|| ligne.startsWith("#")) // 4
continue;
String[] champs = ligne.split(";");
if (champs.length != 4) { // 5
System.err.println("Ligne ignorée : " + ligne); // 6
continue;
}
try {
int annee = Integer.parseInt(...); // 7 (peut lever une exception)
if (annee > 0 // 8
&& annee <= 2026) { // 9
livres.add(...); importes++; // 10
}
} catch (NumberFormatException e) {
System.err.println("Année invalide : " ...); // 11
}
}
return importes; // 12
```
![Graphe J6](images/j6_importer.png)
N = 12 · E = 18 → M = 18 − 12 + 2 = 8
P2 - signe
```python
if x > 0: # 1
return 1 # 2
elif x < 0: # 3
return -1 # 4
return 0 # 5
# 6 fin
```
![Graphe P2](images/p2_signe.png)
N = 6 · E = 7 → M = 7 − 6 + 2 = 3
P3 - notes_valides
La compréhension de liste cache une boucle for et un if. On la réécrit pour voir le graphe :
```python
res = [] # 1
for n in notes: # 2
if 0 <= n <= 20: # 3
res.append(n) # 4
return res # 5
```
![Graphe P3](images/p3_notes_valides.png)
N = 5 · E = 6 → M = 6 − 5 + 2 = 3
2. Comptage des décisions et 3. Classement
exercices.c
| Fonction | Décisions | M | Classe |
|---|---|---|---|
| C1 carre | aucune | 1 | simple |
| C2 moyenne | for + ternaire = 2 | 3 | simple |
| C3 est_bissextile | && + \|\| = 2 | 3 | simple |
| C4 nom_jour | 7 case (default ne compte pas) = 7 | 8 | simple |
| C5 compter_mots | while + 4 \|\| + if + else if = 7 | 8 | simple |
| C6 date_valide | if + 3 \|\| + if (mois == 2) + if (bissextile) + else if + 3 \|\| = 10 | 11 | modéré |
Bibliotheque.java
| Méthode | Décisions | M | Classe |
|---|---|---|---|
| J1 nombreDeLivres | aucune | 1 | simple |
| J2 estDisponible | 2 && = 2 | 3 | simple |
| J3 peutEmprunter | 4 if + ternaire + 2 else if + && = 8 | 9 | simple |
| J4 calculerPenalite | if + \|\| + if + else if + for + if (i >= 2) + if (etudiant) + ternaire = 8 | 9 | simple |
| J5 rechercher | for + 5 if + 2 && + 2 \|\| = 10 | 11 | modéré |
| J6 importer | for + if + \|\| + if + catch + if + && = 7 | 8 | simple |
exercices.py
| Fonction | Décisions | M | Classe |
|---|---|---|---|
| P1 celsius_vers_fahrenheit | aucune | 1 | simple |
| P2 signe | if + elif = 2 | 3 | simple |
| P3 notes_valides | for + if de la compréhension = 2 | 3 | simple |
| P4 mention | for + if = 2 | 3 | simple |
| P5 analyser_mot_de_passe | voir détail ci-dessous = 15 | 16 | modéré |
Détail P5 :
- if len(mdp) < 12 : +1
- for c in mdp : +1
- if c.isupper() + 2 elif : +3
- if not (majuscule and minuscule) : +2 (if + and)
- if not chiffre : +1
- if not special : +1
- if login and ... : +2 (if + and)
- for i in range(...) : +1
- if mdp[i] == mdp[i+1] == mdp[i+2] : +1
- if not problemes : +1
- "moyen" if ... else "faible" (ternaire) : +1
Total = 15 décisions → M = 16.
Aucune fonction n'est complexe (> 20). Trois fonctions sont modérées : C6 date_valide, J5 rechercher et P5 analyser_mot_de_passe (la plus complexe).
4. Cas particuliers
- && et || sans if (C3, J2) : il n'y a aucun if mais l'évaluation est paresseuse. Si la première condition suffit à donner le résultat, la deuxième n'est jamais évaluée. Ça fait donc bien deux chemins différents → +1 par opérateur.
- switch (C4) : chaque case est une sortie possible du switch, donc +1 par case. Le default ne compte pas, c'est le chemin "sinon" (comme le else). 7 cas → M = 8.
- Ternaire (C2, J3, J4, P5) : a ? b : c ou x if c else y est juste un if / else écrit sur une ligne → +1.
- Compréhension de liste (P3) : [n for n in notes if ...] contient une boucle et un test, donc +1 pour le for et +1 pour le if, même si tout tient sur une ligne.
- catch (J6) : Integer.parseInt peut lever une exception, ce qui fait un chemin en plus vers le bloc catch → +1. Le try lui-même ne compte pas.
- continue / break (J5, J6, P5) : ils ne comptent pas, ce sont juste des arêtes vers la boucle ou la sortie. C'est le if qui les contient qui compte.
- Comparaison chaînée en Python (P3, P5) : 0 <= n <= 20 est en fait 0 <= n and n <= 20, donc on pourrait compter un and caché (+1). On ne l'a pas compté, comme Lizard, pour rester cohérent avec l'outil.
5. Refactorisation de P5 - analyser_mot_de_passe
La fonction fait trop de choses à la fois : classer les caractères, vérifier le login, chercher 3 caractères identiques, choisir le niveau. On l'a découpée en petites fonctions qui ont chacune une seule responsabilité (le code complet est dans le fichier refactorisation.py) :
- classer_caracteres(mdp) : parcourt le mot de passe et dit s'il contient une majuscule, une minuscule, un chiffre et un caractère spécial.
- contient_login(mdp, login) : dit si le login est dans le mot de passe.
- a_trois_identiques(mdp) : dit s'il y a 3 caractères identiques à la suite.
- niveau(problemes) : renvoie fort, moyen ou faible selon le nombre de problèmes.
- analyser_mot_de_passe(mdp, login) : met les règles dans une liste (condition, message), garde les messages des règles qui échouent, puis appelle niveau().
```python
def classer_caracteres(mdp):
majuscule = minuscule = chiffre = special = False
for c in mdp:
if c.isupper():
majuscule = True
elif c.islower():
minuscule = True
elif c.isdigit():
chiffre = True
else:
special = True
return majuscule, minuscule, chiffre, special
def contient_login(mdp, login):
return bool(login) and login.lower() in mdp.lower()
def a_trois_identiques(mdp):
for i in range(len(mdp) - 2):
if mdp[i] == mdp[i + 1] == mdp[i + 2]:
return True
return False
def niveau(problemes):
if not problemes:
return "fort"
return "moyen" if len(problemes) <= 2 else "faible"
def analyser_mot_de_passe(mdp, login=None):
majuscule, minuscule, chiffre, special = classer_caracteres(mdp)
regles = [
(len(mdp) < 12, "trop court"),
(not (majuscule and minuscule), "mélanger majuscules et minuscules"),
(not chiffre, "ajouter un chiffre"),
(not special, "ajouter un caractère spécial"),
(contient_login(mdp, login), "ne pas contenir le login"),
(a_trois_identiques(mdp), "éviter 3 caractères identiques"),
]
problemes = [message for echec, message in regles if echec]
return niveau(problemes), problemes
```
Nouveau calcul de M :
| Fonction | Décisions | M |
|---|---|---|
| classer_caracteres | for + if + 2 elif = 4 | 5 |
| contient_login | and = 1 | 2 |
| a_trois_identiques | for + if = 2 | 3 |
| niveau | if + ternaire = 2 | 3 |
| analyser_mot_de_passe | and + for + if de la compréhension = 3 | 4 |
La fonction principale passe de M = 16 (modéré) à M = 4 (simple), et aucune fonction ne dépasse 5. Les règles sont maintenant dans une liste, donc ajouter une règle ne rajoute plus de if.
On a vérifié que le résultat est exactement le même qu'avant en comparant les deux versions sur 200 000 mots de passe aléatoires (avec et sans login) : aucune différence.
6. Bonus : comparaison avec Lizard
Commande : lizard exercices.c Bibliotheque.java exercices.py
| Fonction | Notre M | Lizard (CCN) |
|---|---|---|
| carre | 1 | 1 |
| moyenne | 3 | 3 |
| est_bissextile | 3 | 3 |
| nom_jour | 8 | 8 |
| compter_mots | 8 | 8 |
| date_valide | 11 | 11 |
| nombreDeLivres | 1 | 1 |
| estDisponible | 3 | 3 |
| peutEmprunter | 9 | 9 |
| calculerPenalite | 9 | 9 |
| rechercher | 11 | 11 |
| importer | 8 | 8 |
| celsius_vers_fahrenheit | 1 | 1 |
| signe | 3 | 3 |
| notes_valides | 3 | 3 |
| mention | 3 | 3 |
| analyser_mot_de_passe | 16 | 16 |
Toutes nos valeurs sont identiques à celles de Lizard, parce que Lizard compte les mêmes choses que nous (case, &&, ||, ternaire, catch). Petit détail : Lizard affiche les méthodes Java avec le préfixe Adherent:: alors qu'elles sont dans Bibliotheque, c'est une erreur d'affichage de l'outil (il se trompe de classe à cause des classes internes), mais les valeurs sont bonnes.
Avec lizard -C 10 -w, on obtient seulement les fonctions modérées : date_valide (11), rechercher (11) et analyser_mot_de_passe (16).
Sur refactorisation.py, Lizard donne 5, 2, 3, 3 et 4, comme notre calcul.
Avec lizard -m (un switch compte pour 1) : nom_jour passe de 8 à 2 (1 + 1 pour le switch).
Ce choix nous semble défendable : un switch qui associe juste une valeur à un texte se lit très facilement, et M = 8 exagère sa difficulté (c'est la limite du cours "un switch de 30 cas est lisible, mais vaut M = 31"). C'est aussi ce que fait la complexité cognitive. Par contre, pour les tests, il faut quand même 8 cas pour passer par tous les chemins, donc M = 8 reste la bonne valeur pour savoir combien de tests écrire.
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+52
View File
@@ -0,0 +1,52 @@
"""
Refactorisation de P5 - analyser_mot_de_passe (M = 16 -> 4)
"""
def classer_caracteres(mdp):
majuscule = minuscule = chiffre = special = False
for c in mdp:
if c.isupper():
majuscule = True
elif c.islower():
minuscule = True
elif c.isdigit():
chiffre = True
else:
special = True
return majuscule, minuscule, chiffre, special
def contient_login(mdp, login):
return bool(login) and login.lower() in mdp.lower()
def a_trois_identiques(mdp):
for i in range(len(mdp) - 2):
if mdp[i] == mdp[i + 1] == mdp[i + 2]:
return True
return False
def niveau(problemes):
if not problemes:
return "fort"
return "moyen" if len(problemes) <= 2 else "faible"
def analyser_mot_de_passe(mdp, login=None):
majuscule, minuscule, chiffre, special = classer_caracteres(mdp)
regles = [
(len(mdp) < 12, "trop court"),
(not (majuscule and minuscule), "mélanger majuscules et minuscules"),
(not chiffre, "ajouter un chiffre"),
(not special, "ajouter un caractère spécial"),
(contient_login(mdp, login), "ne pas contenir le login"),
(a_trois_identiques(mdp), "éviter 3 caractères identiques"),
]
problemes = [message for echec, message in regles if echec]
return niveau(problemes), problemes
if __name__ == "__main__":
print(analyser_mot_de_passe("Motdepasse123!", "maxime"))