Files
Forge-Engine/docs/JS_MODERNIZATION_PLAN.md
T
williamandClaude Sonnet 5 66d8eaeae8 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>
2026-09-18 08:46:24 +02:00

135 lines
8.8 KiB
Markdown

# 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`.