Lots 1-3 modernisation JS (S8786/S2703/S2486) + retrait Sonar CI/prod
- 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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
55e81c0fbb
commit
66d8eaeae8
@@ -0,0 +1,134 @@
|
||||
# Plan — lot JS "modernisation" (357 issues SonarQube)
|
||||
|
||||
Trace de la stratégie validée avant exécution — même valeur que
|
||||
`docs/SESSION_RECAP.md` pour la Phase 3 : ce document décrit le plan tel
|
||||
que discuté et confirmé, pas un compte-rendu de ce qui a déjà été fait.
|
||||
Pour l'état d'avancement réel lot par lot, voir les commits de ce
|
||||
chantier (référencés ici au fur et à mesure).
|
||||
|
||||
Origine : rapport SonarQube local (`http://localhost:9000`), catégorie
|
||||
"code smells" JavaScript, 357 occurrences réparties sur 22 règles.
|
||||
|
||||
## 1. Répartition exacte par règle Sonar
|
||||
|
||||
| Règle | Nom | Occurrences |
|
||||
|---|---|---|
|
||||
| `S7773` | Number static methods preferred over globals (`parseFloat`→`Number.parseFloat`, etc.) | 119 |
|
||||
| `S6582` | Optional chaining should be preferred | 98 |
|
||||
| `S7781` | `replaceAll()` instead of `replace()` with global regex | 29 |
|
||||
| `S7761` | Data attributes via `.dataset` | 22 |
|
||||
| `S7765` | `.includes()` instead of `.indexOf()`/`.lastIndexOf()` | 19 |
|
||||
| `S3972` | Conditionals should start on new lines | 15 |
|
||||
| `S3776` | Cognitive Complexity too high | 9 |
|
||||
| `S4138` | `for-of` should be used with Iterables | 6 |
|
||||
| `S4624` | Template literals should not be nested | 6 |
|
||||
| `S2703` | Variables should be declared explicitly (var globale implicite) | 6 |
|
||||
| `S8786` | Regex non-linear backtracking (ReDoS) | 5 |
|
||||
| `S3358` | Ternary operators should not be nested | 4 |
|
||||
| `S7744` | Unnecessary fallback objects in object spread | 3 |
|
||||
| `S2486` | Exceptions should not be ignored | 3 |
|
||||
| `S6557` | `startsWith()`/`endsWith()` instead of manual check | 3 |
|
||||
| `S7721` | Function should be moved to highest possible scope | 2 |
|
||||
| `S4144` | Functions should not have identical implementations | 2 |
|
||||
| `S6653` | `Object.hasOwn()` instead of `hasOwnProperty` | 2 |
|
||||
| `S6671` | Literal used for promise rejection (pas une `Error`) | 1 |
|
||||
| `S7755` | `.at()` instead of `[…length - index]` | 1 |
|
||||
| `S7769` | Modern Math APIs (`Math.hypot` etc.) | 1 |
|
||||
| `S3735` | `void` should not be used | 1 |
|
||||
| **Total** | | **357** |
|
||||
|
||||
## 2. Classification
|
||||
|
||||
### Mécanique, sans risque sémantique (équivalence prouvée)
|
||||
|
||||
| Règle | Pourquoi c'est sûr |
|
||||
|---|---|
|
||||
| `S7773` | `parseFloat`/`parseInt`/`isNaN`/`isFinite` sont des alias exacts de `Number.*` depuis ES2015 |
|
||||
| `S7781` | `replace()`→`replaceAll()` seulement quand la regex a déjà le flag `g` (précondition de la règle) |
|
||||
| `S7765` | `.indexOf(x) !== -1` ⇔ `.includes(x)` — équivalence stricte pour ce pattern |
|
||||
| `S7761` | `getAttribute('data-x')`→`.dataset.x` — mécanique si nom d'attribut statique |
|
||||
| `S7744` | Le spread de `null`/`undefined` ne lève jamais — le fallback `\|\| {}` est toujours redondant |
|
||||
| `S6557`, `S6653`, `S7755`, `S7769` | Équivalences directes, très faible volume (7 sites), pas besoin d'outil |
|
||||
| `S3972` | Pur formatage, zéro impact comportemental |
|
||||
|
||||
### ⚠️ Correction par rapport à l'hypothèse de départ : `S6582` n'est PAS mécanique
|
||||
|
||||
`a && a.b` et `a?.b` divergent quand `a` est une valeur **fausse mais
|
||||
définie** (`0`, `""`, `false`, `NaN`) : `a && a.b` s'arrête et renvoie
|
||||
`a`, `a?.b` continue et accède à `.b`. Une transformation aveugle peut
|
||||
changer le comportement. Malgré son volume (98, 2ᵉ plus grosse règle),
|
||||
`S6582` est traité en examen au cas par cas, pas en lot mécanique.
|
||||
|
||||
### Nécessite un vrai examen au cas par cas
|
||||
|
||||
| Règle | Nature | Note |
|
||||
|---|---|---|
|
||||
| `S6582` (98) | Sémantique | Voir ci-dessus — sous-lots avec vérification du type de la valeur testée |
|
||||
| `S8786` (5) | **Sécurité-adjacent** | Sites JS, distincts de l'exception Python déjà validée en Phase 3 (`auth/email_validation.py`) |
|
||||
| `S2703` (6) | **Sécurité-adjacent** | 1 seul des 6 sites déjà examiné et validé (`bindings.js:179`, `gameData`) — 5 restent à vérifier, ne pas supposer qu'ils sont identiques |
|
||||
| `S2486` (3) | **Sécurité-adjacent** | Exception avalée, peut masquer un vrai bug |
|
||||
| `S3776` (9) | Refactor réel | Pas de fix mécanique — restructuration fonction par fonction |
|
||||
| `S4624` (6), `S3358` (4) | Lisibilité | Extraction manuelle |
|
||||
| `S4138` (6) | Risque de perte de l'index | Fix proposé par un outil mais vérifié site par site avant application |
|
||||
| `S4144` (2), `S7721` (2) | Décision de conception | Dédupliquer/déplacer une fonction change la portée |
|
||||
| `S6671` (1) | Change le type de la valeur de rejet | Impact possible sur du code aval qui inspecterait `.message`/`instanceof Error` |
|
||||
| `S3735` (1) | Dépend de l'intention | À lire avant de trancher |
|
||||
|
||||
## 3. Outillage — vérifié empiriquement
|
||||
|
||||
- **`eslint-plugin-sonarjs`** (officiel SonarSource, 4.2.1, dernière version) : ne couvre qu'**une seule** des 22 règles avec un fixer (`S3972`) — les règles `S77xx` sont trop récentes pour cette version.
|
||||
- **`eslint-plugin-unicorn`** : dernière version (75.0.0) exige ESLint ≥10 (incompatible avec notre 8.57.1). Version retenue : **55.0.0** (exige ESLint ≥8.56.0, compatible), ajoutée en devDependency permanente. 9 règles activées dans `.eslintrc.json` avec un fixer confirmé (`fixable: 'code'` vérifié dans le code source du plugin) :
|
||||
- `unicorn/prefer-number-properties` (S7773)
|
||||
- `unicorn/prefer-string-replace-all` (S7781)
|
||||
- `unicorn/prefer-dom-node-dataset` (S7761)
|
||||
- `unicorn/prefer-includes` (S7765)
|
||||
- `unicorn/prefer-string-starts-ends-with` (S6557)
|
||||
- `unicorn/prefer-modern-math-apis` (S7769)
|
||||
- `unicorn/prefer-at` (S7755)
|
||||
- `unicorn/no-useless-fallback-in-spread` (S7744)
|
||||
- `unicorn/no-for-loop` (S4138)
|
||||
- Uniquement ces 9 règles activées, jamais la config `recommended` complète du plugin (qui en contient des dizaines d'autres, jamais évaluées pour ce projet) — voir `CODE_QUALITY.md` section 2.
|
||||
- **Dry-run réel** (`--fix-dry-run`) sur tout `static/js/**/*.js` avec ces 9 règles : **222 problèmes détectés, 222 avec un fix proposé.** Les comptes par règle ne correspondent pas exactement à ceux de Sonar (moteurs différents, ex. `prefer-includes` détecte 41 cas contre 19 pour `S7765` côté Sonar) — normal, la vérification finale se fait via un rescan Sonar après application, pas via le compte ESLint.
|
||||
- **`@typescript-eslint`** (candidat pour combler `S6582`) : nécessite de remplacer tout le parser JS par le parser TypeScript — écarté, trop lourd pour un projet 100% JS.
|
||||
- **`Object.hasOwn` (`S6653`, 2 sites) et `startsWith`/`endsWith` (`S6557`, 3 sites)** : aucun outil ne les a détectés dans le dry-run — 5 sites au total, correction manuelle directe.
|
||||
|
||||
## 4. Ordre de priorité
|
||||
|
||||
1. Sécurité-adjacent : `S8786` (5), `S2703` (6, dont 5 non-vérifiés), `S2486` (3) — 14 sites.
|
||||
2. Structure/lisibilité à risque de bug : `S3776` (9), `S4624` (6), `S3358` (4), `S4144` (2), `S7721` (2), `S6671` (1), `S3735` (1) — 25 sites.
|
||||
3. `S6582` optional chaining (98) — lot dédié à part.
|
||||
4. Modernisation mécanique via outil (`S7773`, `S7781`, `S7765`, `S7761`, `S7744`, `S3972`, `S4138` vérifié) — le plus gros volume, le moins risqué, en dernier.
|
||||
5. Reliquat manuel très faible volume (`S6557`, `S6653`, `S7755`, `S7769`) — 7 sites.
|
||||
|
||||
## 5. Découpage en 9 lots
|
||||
|
||||
| Lot | Contenu | Méthode |
|
||||
|---|---|---|
|
||||
| 1 | `S8786` (5) | Lecture individuelle, diff par site |
|
||||
| 2 | `S2703` (5 restants, hors `gameData` déjà validé) | Lecture individuelle — même pattern ou différent ? |
|
||||
| 3 | `S2486` (3) | Lecture individuelle |
|
||||
| 4 | `S3776` (9) | Refactor fonction par fonction |
|
||||
| 5 | `S4624` + `S3358` + `S4144` + `S7721` + `S6671` + `S3735` (16) | Groupés, petits volumes, revue individuelle |
|
||||
| 6 | `S6582` (98) | Sous-lots de ~20-25, vérification du type à chaque site |
|
||||
| 7 | `S7773`+`S7781`+`S7765`+`S7761`+`S7744`+`S3972` via `eslint-plugin-unicorn`@55.0.0 `--fix` | `--fix` en un coup, diff complet relu avant commit |
|
||||
| 8 | `S4138` (fix proposé par `no-for-loop`, vérifié un par un) | Semi-automatique |
|
||||
| 9 | `S6557`+`S6653`+`S7755`+`S7769` (7) | Manuel direct |
|
||||
|
||||
**À chaque lot** : diff complet montré avant application, suite de tests
|
||||
JS (`node --test static/js/play/__tests__/*.test.js
|
||||
static/js/play/offline/__tests__/*.test.js
|
||||
static/js/scenes/__tests__/*.test.js`, 241 tests) après application,
|
||||
signalement immédiat de tout changement de comportement réel (pas
|
||||
seulement du style). Rescan SonarQube local après le lot 7 (le plus gros
|
||||
volume mécanique) pour confirmer la réduction réelle plutôt que de se
|
||||
fier au seul compte ESLint, puis un rescan final après le lot 9.
|
||||
|
||||
## Note de séquencement Git
|
||||
|
||||
Les 9 règles `unicorn/*` ont été activées dans `.eslintrc.json` (et
|
||||
`eslint-plugin-unicorn` ajouté en devDependency) **avant** que les 222
|
||||
occurrences correspondantes soient corrigées — ce qui rend `pre-commit`/
|
||||
la CI rouges sur ESLint dès maintenant si ces changements de config
|
||||
étaient commités seuls. Ces changements restent donc **non commités**
|
||||
tant que le lot 7 (la correction mécanique) n'est pas fait, pour ne
|
||||
jamais introduire un commit où le lint échouerait sur `dev`.
|
||||
@@ -0,0 +1,242 @@
|
||||
# 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).
|
||||
@@ -0,0 +1,57 @@
|
||||
// Script de verification empirique pour les 5 sites NOSONAR javascript:S8786
|
||||
// (voir CODE_QUALITY.md, section 5) - teste les 3 regex distinctes flaguees
|
||||
// (2 sont dupliquees dans 2 fichiers chacune) contre des entrees construites
|
||||
// pour maximiser l'ambiguite que l'analyseur Sonar soupconne, a plusieurs
|
||||
// tailles croissantes. Un vrai ReDoS montrerait une croissance exponentielle
|
||||
// du temps avec la taille (x10 la taille -> beaucoup plus que x10 le temps).
|
||||
//
|
||||
// A relancer si l'une de ces 3 regex change de forme, pour confirmer que le
|
||||
// NOSONAR reste justifie plutot que de le recopier aveuglement.
|
||||
//
|
||||
// Usage : node docs/redos_probe_s8786.js
|
||||
|
||||
function timeIt(label, fn) {
|
||||
const start = process.hrtime.bigint();
|
||||
const result = fn();
|
||||
const end = process.hrtime.bigint();
|
||||
const ms = Number(end - start) / 1e6;
|
||||
console.log(`${label}: ${ms.toFixed(2)}ms -> match=${result !== null}`);
|
||||
return ms;
|
||||
}
|
||||
|
||||
console.log('=== Pattern A (filter-repeater-rows.js:10, panel-init.js:254) ===');
|
||||
const patternA = /^\{\{\s*([^.{}]+)\.([^.{}]+)\s*\}\}$/;
|
||||
for (const n of [1000, 10000, 50000, 100000]) {
|
||||
const evil = `{{${'a'.repeat(n)}`; // pas de point, pas de fermeture
|
||||
timeIt(` n=${n} (pas de point, pas de fermeture)`, () => patternA.exec(evil));
|
||||
}
|
||||
for (const n of [1000, 10000, 50000, 100000]) {
|
||||
const evil = `{{${'a'.repeat(n)}.${'b'.repeat(n)}`; // point present, pas de fermeture
|
||||
timeIt(` n=${n} (point present, pas de fermeture)`, () => patternA.exec(evil));
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('=== Pattern B (filter-repeater-rows.js:11, panel-init.js:261) ===');
|
||||
const patternB = /^\{\{\s*\$([^.{}[\]]+)((?:\.[^.{}[\]]+|\[\d+\])*)\s*\}\}$/;
|
||||
for (const n of [1000, 5000, 10000, 20000]) {
|
||||
const evil = `{{$a${'.b'.repeat(n)}`; // repetition simple, pas de fermeture
|
||||
timeIt(` n=${n} (${n} segments ".b", pas de fermeture)`, () => patternB.exec(evil));
|
||||
}
|
||||
for (const n of [1000, 5000, 10000, 20000]) {
|
||||
const evil = `{{$a${'.b[0]'.repeat(n)}`; // alternance des deux branches
|
||||
timeIt(` n=${n} (${n} segments ".b[0]" alternes, pas de fermeture)`, () => patternB.exec(evil));
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('=== Pattern C (xapi-client.js:132) ===');
|
||||
const patternC = /\/+$/;
|
||||
for (const n of [10000, 100000, 1000000]) {
|
||||
const evil = '/'.repeat(n);
|
||||
timeIt(` n=${n} (que des slashes)`, () => patternC.exec(evil));
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('=== Verification d\'equivalence (entrees valides normales, comportement inchange attendu) ===');
|
||||
console.log('A sur "{{Objet.champ}}":', patternA.exec('{{Objet.champ}}'));
|
||||
console.log('B sur "{{$var.champ[0].sous}}":', patternB.exec('{{$var.champ[0].sous}}'));
|
||||
console.log('C sur "https://host///":', 'https://host///'.replace(patternC, ''));
|
||||
Reference in New Issue
Block a user