diff --git a/COMPTE_RENDU.md b/COMPTE_RENDU.md new file mode 100644 index 0000000..d3b92b8 --- /dev/null +++ b/COMPTE_RENDU.md @@ -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. diff --git a/images/c2_moyenne.png b/images/c2_moyenne.png new file mode 100644 index 0000000..64e0506 Binary files /dev/null and b/images/c2_moyenne.png differ diff --git a/images/c4_nom_jour.png b/images/c4_nom_jour.png new file mode 100644 index 0000000..ffb47df Binary files /dev/null and b/images/c4_nom_jour.png differ diff --git a/images/j2_estDisponible.png b/images/j2_estDisponible.png new file mode 100644 index 0000000..1de827b Binary files /dev/null and b/images/j2_estDisponible.png differ diff --git a/images/j6_importer.png b/images/j6_importer.png new file mode 100644 index 0000000..6a5b478 Binary files /dev/null and b/images/j6_importer.png differ diff --git a/images/p2_signe.png b/images/p2_signe.png new file mode 100644 index 0000000..fcc5f58 Binary files /dev/null and b/images/p2_signe.png differ diff --git a/images/p3_notes_valides.png b/images/p3_notes_valides.png new file mode 100644 index 0000000..5f83a0d Binary files /dev/null and b/images/p3_notes_valides.png differ diff --git a/refactorisation.py b/refactorisation.py new file mode 100644 index 0000000..900001a --- /dev/null +++ b/refactorisation.py @@ -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"))