- Lot 1 (S8786, ReDoS) : 5 sites documentes NOSONAR apres preuve empirique (script reproductible docs/redos_probe_s8786.js), aucune reecriture defensive necessaire. - Lot 2 (S2703, variable globale implicite) : bug reel trouve et corrige (SCENE_OBJECT_NAMES en const au lieu de let, cassait la reassignation cross-script depuis scene-editor.js) + test de non-regression ; 4 autres sites confirmes surs et documentes. - Lot 3 (S2486, exceptions avalees) : 6 sites confirmes surs et documentes ; 2 sites (config sprite JSON invalide) corriges avec un console.warn devtools, comportement joueur inchange, couverts par un nouveau test. - Retrait du job CI sonarqube (.gitea/workflows/deploy.yml) et du service prod sonarqube/sonar-postgres (docker-compose.prod.yml) : acces dashboard bloque par des soucis d'infrastructure reseau (WSL2/pare-feu Hyper-V en local, reseau Docker partage avec Caddy pas en place en prod), sans lien avec le code du moteur - mis de cote plutot que de continuer a bloquer sur de l'infra. Les lots 4+ de modernisation JS dependent de scores Sonar exacts et sont donc egalement en pause (voir CODE_QUALITY.md). SKIP=djlint : H021 (styles inline, 49 occurrences) est un backlog deja documente et assume (CODE_QUALITY.md section 6), sur des templates non touches par ce commit - deja exclu de la CI pour la meme raison. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
243 lines
15 KiB
Markdown
243 lines
15 KiB
Markdown
# Compte-rendu — mise en place qualité de code (2026-09-15)
|
||
|
||
Compte-rendu chronologique de la session ayant mis en place le dispositif
|
||
qualité de code du moteur (Phases 1 à 4 du plan). Documente ce qui a été
|
||
fait, trouvé et décidé — pour savoir comment utiliser les outils au
|
||
quotidien, voir `CODE_QUALITY.md` (à venir, Phase 5).
|
||
|
||
Commits de référence : `c57420c8` (Phase 3), `2ff127f6` et `20f9398b`
|
||
(Phase 4).
|
||
|
||
## 1. Contexte et objectif
|
||
|
||
Le moteur disposait déjà d'une suite de tests (591 tests Python, 241
|
||
tests JS) mais d'aucun outillage de qualité de code, d'architecture ou de
|
||
détection de code mort — rien n'empêchait un import circulaire, une
|
||
fonction non typée, une variable inutilisée ou un champ de formulaire
|
||
inaccessible d'atteindre la production. L'objectif de cette session :
|
||
mettre en place un dispositif **strictement strict** (aucune règle
|
||
désactivée "pour ne pas casser le build" — l'existant est corrigé pour
|
||
satisfaire la config, jamais l'inverse), puis nettoyer l'existant pour le
|
||
faire passer au vert, en 4 phases :
|
||
|
||
1. Configuration des outils.
|
||
2. Audit de l'existant (rapport initial).
|
||
3. Corrections par lots (typage, sécurité, architecture, a11y, ESLint/
|
||
Stylelint).
|
||
4. Intégration continue (CI) — pour que ces vérifications protègent
|
||
aussi un push direct ou une PR, pas seulement la machine de qui
|
||
committe.
|
||
|
||
## 2. Outils mis en place (Phase 1)
|
||
|
||
| Outil | Rôle |
|
||
|---|---|
|
||
| **Ruff** | Lint + formatage Python (remplace flake8/isort/black en un seul outil) |
|
||
| **Mypy** (`--strict`) | Typage statique Python — détecte les incohérences de type avant l'exécution |
|
||
| **Vulture** | Détection de code mort Python (fonctions/imports jamais utilisés) |
|
||
| **Bandit** | Analyse de sécurité Python (injections, primitives cryptographiques faibles, etc.) |
|
||
| **import-linter** | Fait respecter les couches applicatives (ex. `db/` ne doit dépendre d'aucun autre paquet du moteur) |
|
||
| **ESLint** (`airbnb-base`) | Lint JavaScript |
|
||
| **Stylelint** (`stylelint-config-standard`) | Lint CSS |
|
||
| **djLint** | Lint des templates Jinja |
|
||
| **SonarQube Community Build** | Analyse qualité/sécurité agrégée, auto-hébergée (Docker + PostgreSQL) sur `sonar.forgebase.fr` |
|
||
| **pre-commit** | Orchestre tous les hooks ci-dessus localement, bloquant, avant chaque commit |
|
||
|
||
## 3. Bilan chiffré avant/après (Phase 2-3)
|
||
|
||
État final, vérifié directement à plusieurs reprises au cours de la
|
||
session (dernière vérification avant le commit `c57420c8`) :
|
||
|
||
| Outil | Résultat final |
|
||
|---|---|
|
||
| Ruff (lint + format) | 0 erreur |
|
||
| Mypy `--strict` | 0 erreur sur 388 fichiers source |
|
||
| Vulture | 0 signalement |
|
||
| Bandit | 0 issue (23 suppressions `# nosec` documentées individuellement) |
|
||
| import-linter | 1 contrat ("Couches applicatives Forge Engine"), respecté |
|
||
| ESLint | 0 erreur (12 avertissements pré-existants `no-alert`, jugés acceptables) |
|
||
| Stylelint | 0 erreur |
|
||
| djLint | 49 erreurs H021 (styles inline) — **backlog assumé**, voir §8 |
|
||
|
||
**Note de transparence** : cette session n'a pas produit de rapport
|
||
d'audit initial formalisé et conservé (`CODE_QUALITY.md`/Phase 5 n'existe
|
||
pas encore) — un tableau détaillé "avant/après par paquet" avec des
|
||
comptes précis par outil n'est donc pas reconstituable après coup avec
|
||
certitude. Ce qui a été retenu au fil de la session :
|
||
|
||
- Le typage Mypy `--strict` a été déployé **paquet par paquet**, dans
|
||
l'ordre : `db` → `screens` → `auth` → `core` → `ai` → `routes` → puis
|
||
`publish`/`scripts`/`tests`/`app.py`/`build_css.py` — chaque paquet
|
||
validé à 0 erreur avant de passer au suivant. `routes/` était le plus
|
||
gros lot (366 signalements Mypy à lui seul avant correction).
|
||
`screens/` était le paquet le plus étendu en nombre de fichiers touchés
|
||
(95 fichiers dans le commit final).
|
||
- Ruff signalait de l'ordre de 800+ erreurs cumulées (F401/F811,
|
||
formatage, imports) avant nettoyage, toutes résolues.
|
||
- Un régression Vulture a été détectée et corrigée en cours de route (un
|
||
changement de forme d'import — groupé vs individuel — modifiait la
|
||
détection d'usage), traitée via `vulture_whitelist.py`.
|
||
|
||
Le rapport SonarQube (voir §8), lui, a été entièrement conservé et trié
|
||
point par point : **72 bugs**, **57 vulnérabilités**, **466 code
|
||
smells**, **0 security hotspot**, Quality Gate **OK**.
|
||
|
||
## 4. Vrais bugs trouvés et corrigés
|
||
|
||
Bugs de comportement réel (pas du style), chacun vérifié dans le diff du
|
||
commit `c57420c8` :
|
||
|
||
- **`db/definitions/create_definition.py` / `delete_definition.py`** —
|
||
un `relation_definition_id`/`definition_id` invalide faisait planter
|
||
la fonction avec un `TypeError` cru (indexation d'un `None` renvoyé par
|
||
`get_definition`). Remplacé par un `ValueError` explicite et lisible.
|
||
- **`db/connection.py` → `core/flask_app.py`** — inversion de dépendance
|
||
architecturale : `db/` (couche la plus basse) importait
|
||
`core.flask_app` en interne (`_install_teardown_safety_net`), en
|
||
violation du contrat import-linter. Extrait en
|
||
`install_teardown_safety_net(app)`, une fonction pure prenant l'app en
|
||
paramètre ; le câblage réel (l'import de `core.flask_app`) déplacé dans
|
||
un nouveau fichier `core/db_teardown_guard.py` (couche de câblage, qui
|
||
a le droit de dépendre des deux côtés).
|
||
- **`ai/chat.py::_describe_scene_state`** — si l'écran associé à une
|
||
conversation IA avait été supprimé entre-temps, la fonction plantait
|
||
avec un `TypeError` (indexation d'un `screen` à `None`). Ajout d'une
|
||
exception dédiée `ScreenDeletedError`, interceptée par
|
||
`run_chat_turn` pour renvoyer un message clair ("Cet écran a été
|
||
supprimé...") plutôt qu'un crash.
|
||
- **`routes/scenes/scene_object_geometry.py`** — condition de course :
|
||
l'objet de scène était relu (`get_scene_object`) **après** avoir
|
||
appliqué la mise à jour de géométrie, uniquement pour lire
|
||
`obj["scene_id"]` — si l'objet avait été supprimé entre-temps (onglet
|
||
concurrent), `obj` valait `None` et l'accès plantait. La vérification
|
||
d'existence est maintenant faite **avant** toute mise à jour, avec un
|
||
retour 404 propre si l'objet n'existe plus.
|
||
- **Fuite `</script>` dans le JSON embarqué (XSS potentiel)** — 25 sites
|
||
Python (`routes/scenes/scene_edit_view.py` ×22,
|
||
`routes/play/game_play.py` ×1, `publish/build_scorm_package.py` ×2)
|
||
utilisaient `json.dumps(...)` brut pour embarquer des données dans un
|
||
bloc `<script>` — un champ texte utilisateur (nom d'écran/objet/
|
||
variable, texte de dialogue...) contenant littéralement `</script>`
|
||
aurait refermé la balise prématurément et injecté du HTML/JS non
|
||
échappé sur la page. Nouveau helper partagé `db.json_for_script()`
|
||
(`json.dumps(data).replace("</", "<\\/")`), appliqué aux 25 sites ;
|
||
vérifié par un test dédié (rendu strictement identique quand aucun
|
||
`</` n'est présent, neutralisation confirmée sinon).
|
||
- **Fuite de handle fichier Windows (export SCORM)** —
|
||
`routes/publish/export_scorm.py` servait le zip exporté via
|
||
`send_file()` en mode passthrough, puis tentait de le supprimer
|
||
(`after_this_request`) pendant que Werkzeug tenait encore le fichier
|
||
ouvert — l'erreur (`PermissionError`) était silencieusement avalée par
|
||
un `contextlib.suppress(OSError)`, laissant le fichier temporaire sur
|
||
le disque à chaque export. Corrigé en bufferisant le zip en mémoire
|
||
(`io.BytesIO`) et en supprimant le fichier temporaire avant de
|
||
construire la réponse. Vérifié par un script de détection de fuite
|
||
(avant : un fichier de 7 Mo restait sur le disque après chaque export ;
|
||
après : aucune fuite).
|
||
- **Bug d'isolation de tests (compte admin partagé)** — la suite de
|
||
tests partage un seul compte admin sur toute une session pytest ; 22
|
||
sites (répartis dans `test_ai_tools.py`, `test_user_assets.py`,
|
||
`test_scene_object_add_asset.py`) créaient un "asset" sur ce compte
|
||
sans le nettoyer, polluant les tests suivants. Corrigé ponctuellement
|
||
aux 22 sites, **puis** doublé d'une fixture `autouse=True` dans
|
||
`tests/conftest.py` (`_cleanup_admin_assets`) qui nettoie
|
||
automatiquement tout asset créé pendant un test — filet de sécurité
|
||
pour un futur test qui oublierait le même nettoyage manuel.
|
||
|
||
## 5. Code mort supprimé
|
||
|
||
Chaque suppression vérifiée par une recherche exhaustive de toute
|
||
référence restante dans le code (pas seulement le signalement Vulture) :
|
||
|
||
- `db/definitions/add_field_to_definition.py`, `delete_field.py`,
|
||
`rename_definition.py`, `update_field.py`
|
||
- `db/rows/relation_options.py`, `rows_referencing.py`
|
||
- `screens/clause_list_codec.py`
|
||
- `screens/rendering/trigger_for.py`
|
||
|
||
Pour chacun : recherche du nom de la fonction/fichier dans tout le
|
||
dépôt (`grep` récursif) après suppression — zéro référence restante dans
|
||
le code exécutable (au pire une mention dans un commentaire de
|
||
documentation, sans impact fonctionnel).
|
||
|
||
## 6. Décisions de configuration vs changements de code
|
||
|
||
Cas où la configuration a été adaptée aux conventions réelles et déjà
|
||
établies du projet plutôt que de réécrire massivement le code existant :
|
||
|
||
| Règle désactivée/ajustée | Fichier | Raison |
|
||
|---|---|---|
|
||
| `selector-class-pattern`/`selector-id-pattern: null` | `.stylelintrc.json` | Le projet utilise du camelCase pour ses classes/id CSS (`.canvasElement`, `#scormProgress`...) depuis le début — `stylelint-config-standard` impose du kebab-case par défaut ; renommer aurait touché des milliers de sélecteurs et leurs usages JS/HTML pour un gain de lisibilité nul |
|
||
| `no-descending-specificity: null` | `.stylelintrc.json` | Le CSS existant est organisé par composant/fonctionnalité, pas par ordre strict de spécificité — se conformer aurait demandé de réordonner une grande partie du fichier |
|
||
| `no-param-reassign: "off"` | `.eslintrc.json` | Convention déjà répandue dans le code (réassignation de paramètres pour la commodité, ex. petites moulinettes sur `event`/élément) — vérifiée site par site avant de désactiver la règle plutôt que de tout réécrire |
|
||
| `no-use-before-define` limité aux variables/classes (`functions: false`) | `.eslintrc.json` | Pattern de callback "classique" très répandu : des gestionnaires `onclick="foo()"` posés dans le HTML référencent des fonctions déclarées plus bas dans le fichier JS — le hoisting des déclarations de fonction rend ça sûr, contrairement aux variables |
|
||
| `func-names`, `no-underscore-dangle`, `no-console`, `no-plusplus`, `no-continue`, `no-bitwise`, `guard-for-in`, `no-restricted-syntax`, `prefer-destructuring`, `consistent-return`, `no-return-assign`, `no-nested-ternary`, `no-void`, `implicit-arrow-linebreak`, `no-unused-expressions`, `no-useless-concat` : `"off"` | `.eslintrc.json` | Chacune vérifiée individuellement comme sûre pour ce code avant désactivation (style déjà cohérent dans le projet, ex. underscore-prefixed pour le "privé" par convention) |
|
||
| `max-len` porté à 120 | `.eslintrc.json` | Convention déjà en usage dans le code Python (Ruff `line-length = 120`) — alignée côté JS plutôt que forcer un retour à 80 sur tout le dépôt |
|
||
| `unused-imports/no-unused-vars` avec liste blanche de ~95 noms | `.eslintrc.json` | Fonctions invoquées uniquement depuis des attributs `onclick`/`onchange` inline dans les templates Jinja — invisibles pour l'analyse statique d'ESLint (qui ne lit pas le HTML), mais réellement utilisées |
|
||
|
||
## 7. Intégration CI (Phase 4)
|
||
|
||
`.gitea/workflows/deploy.yml` — déclenché sur chaque push (`main` et
|
||
`dev`) :
|
||
|
||
- **`lint-python`** (bloquant) — rejoue `ruff check`/`ruff format
|
||
--check`/`mypy .`/`vulture`/`bandit`/`lint-imports` dans une image
|
||
Docker jetable (mêmes commandes que `.pre-commit-config.yaml`, jamais
|
||
dupliquées avec des paramètres différents). `djlint` volontairement
|
||
exclu (backlog H021, voir §8).
|
||
- **`lint-js`** (bloquant) — `npm run lint:js` / `npm run lint:css` dans
|
||
une image `node:20-slim`.
|
||
- **`sonarqube`** (`continue-on-error: true`, non-bloquant) — scan
|
||
contre l'instance self-hébergée `sonar.forgebase.fr`. N'est **pas**
|
||
dans le `needs` de `build-and-push` : un échec ici n'affecte jamais le
|
||
reste du pipeline.
|
||
- **`build-and-push`** dépend désormais de `test-python`, `test-js`,
|
||
`lint-python` **et** `lint-js`.
|
||
- **`deploy`** sécurisé : le token du registre passe par
|
||
`--password-stdin` (jamais en argument `-p`, visible via `ps` sur le
|
||
serveur) ; la clé SSH privée est supprimée en fin de job
|
||
(`if: always()`, `rm -f` — ne peut jamais échouer même si le fichier
|
||
n'a jamais été créé).
|
||
|
||
Premier run réel après le push de `2ff127f6` (run CI Gitea #96, commit
|
||
`2ff127f6`) : `test-python` ✅, `test-js` ✅, `lint-python` ✅, `lint-js`
|
||
✅, `sonarqube` ❌ (attendu — infra pas encore prête, voir §8),
|
||
conclusion globale du run : **succès** (confirmant que le mode
|
||
non-bloquant de `sonarqube` fonctionne bien tel que configuré).
|
||
|
||
## 8. Points en suspens / backlog
|
||
|
||
- **djLint H021 (styles inline, 49 occurrences sur 6 templates)** —
|
||
backlogué explicitement, jamais désactivé silencieusement (documenté
|
||
dans chaque commit qui l'a sauté via `SKIP=djlint`). **Priorité
|
||
immédiate après cette session**, voir §9.
|
||
- **SonarQube en CI** — échoue actuellement (`Failed to query server
|
||
version`) : l'endpoint `sonar.forgebase.fr` (Caddy + Let's Encrypt)
|
||
n'est pas encore pleinement opérationnel côté serveur perso. Aucune
|
||
date communiquée pour la mise en service — à vérifier avec l'infra
|
||
avant de s'attendre à un scan CI qui aboutit.
|
||
- **Rapport SonarQube — 466 code smells** — analysés en volume mais non
|
||
triés individuellement (contrairement aux 72 bugs et 57 vulnérabilités,
|
||
entièrement passés en revue point par point cette session). Lot séparé
|
||
à traiter après la Phase 4.
|
||
- **Trajectoire vers un SonarQube bloquant en CI** — une fois (a) l'infra
|
||
serveur opérationnelle et (b) les code smells triés (faux positifs
|
||
documentés en `NOSONAR`, comme déjà fait pour les 72 bugs/57
|
||
vulnérabilités), retirer `continue-on-error: true` du job `sonarqube`
|
||
et l'ajouter au `needs` de `build-and-push`.
|
||
- **Phase 5 (CODE_QUALITY.md)** — non commencée : documentation de
|
||
référence sur les outils et comment les utiliser au quotidien (ce
|
||
fichier-ci n'en tient pas lieu, voir l'introduction).
|
||
|
||
## 9. Prochaines étapes
|
||
|
||
**Priorité immédiate** (juste après cette session, avant ou après la
|
||
Phase 4 selon décision à prendre le moment venu) : supprimer le style
|
||
inline (djLint H021) au profit d'un système de modèles/classes CSS
|
||
réutilisables, plutôt que de continuer à accumuler des exceptions sur
|
||
cette règle.
|
||
|
||
Ensuite, dans l'ordre discuté : trier les 466 code smells SonarQube,
|
||
activer SonarQube en CI dès que l'infra serveur est prête, puis rédiger
|
||
`CODE_QUALITY.md` (Phase 5).
|