@silvestv/migration-planificator 6.0.0 → 6.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/src/core/app-analyzer.js +10 -2
  2. package/dist/src/core/ast/matchers/html/html-template-ref-collector.js +95 -0
  3. package/dist/src/core/ast/matchers/html/html-text-matcher.js +85 -32
  4. package/dist/src/core/ast/matchers/html/index.js +16 -10
  5. package/dist/src/core/ast/matchers/index.js +44 -0
  6. package/dist/src/core/ast/matchers/ts/collection-matcher.js +4 -0
  7. package/dist/src/core/ast/matchers/ts/context-matcher.js +7 -4
  8. package/dist/src/core/ast/matchers/ts/decorator-matcher.js +145 -144
  9. package/dist/src/core/ast/matchers/ts/file-matcher.js +199 -8
  10. package/dist/src/core/ast/matchers/ts/host-binding-property-matcher.js +327 -0
  11. package/dist/src/core/ast/matchers/ts/mutation-matcher.js +248 -0
  12. package/dist/src/core/ast/matchers/ts/node-matcher.js +17 -1
  13. package/dist/src/core/ast/matchers/ts/symbol-matcher.js +98 -2
  14. package/dist/src/core/ast/matchers/ts/type-matcher.js +5 -2
  15. package/dist/src/core/ast/matchers/utils/matcher-helpers.js +60 -0
  16. package/dist/src/core/ast/matchers/utils/template-cache.js +50 -0
  17. package/dist/src/core/project-detector.js +22 -10
  18. package/dist/src/core/project-strategy/nx-strategy.js +25 -22
  19. package/dist/src/data/angular-migration-rules.json +32 -57
  20. package/dist/src/data/rules/rearchitecture/rearchitecture-rules.json +30 -0
  21. package/dist/src/data/rules/to18/rules-18-obligatoire.json +8 -14
  22. package/dist/src/data/rules/to18/rules-18-optionnelle.json +0 -56
  23. package/dist/src/data/rules/to18/rules-18-recommande.json +32 -139
  24. package/dist/src/data/rules/to19/rules-19-obligatoire.json +4 -1
  25. package/dist/src/data/rules/to19/rules-19-optionnelle.json +3 -0
  26. package/dist/src/data/rules/to19/rules-19-recommande.json +30 -2
  27. package/dist/src/data/rules/to20/rules-20-optionnelle.json +0 -35
  28. package/dist/src/data/rules/to20/rules-20-recommande.json +44 -36
  29. package/dist/src/data/rules/to21/rules-21-obligatoire.json +23 -10
  30. package/package.json +1 -1
@@ -139,7 +139,7 @@ function resolveSpecifierOneLevel(specifier) {
139
139
  /**
140
140
  * Vérifie si un nœud référence un symbole qui correspond au pattern referTo
141
141
  * @param node Nœud à vérifier (généralement un Identifier dans un tableau providers)
142
- * @param referToPattern Pattern de référence (decorator, propertyNotEqual, etc.)
142
+ * @param referToPattern Pattern de référence (decorator, propertyNotEqual, functionBody, etc.)
143
143
  * @returns true si le symbole référencé matche le pattern
144
144
  */
145
145
  function matchesReferTo(node, referToPattern) {
@@ -149,7 +149,20 @@ function matchesReferTo(node, referToPattern) {
149
149
  const definitions = identifierNode.getDefinitionNodes();
150
150
  if (definitions.length === 0)
151
151
  return false;
152
- return definitions.some((def) => matchesDefinition(def, referToPattern));
152
+ return definitions.some((def) => {
153
+ // Mode existant : décorateur de classe
154
+ if (referToPattern.decorator) {
155
+ return matchesDefinition(def, referToPattern);
156
+ }
157
+ // NOUVEAU : analyse du body de fonction (guards fonctionnels)
158
+ if (referToPattern.functionBody) {
159
+ const body = resolveToFunctionBody(def);
160
+ if (!body)
161
+ return false;
162
+ return checkFunctionBodyMatch(body, referToPattern.functionBody);
163
+ }
164
+ return false;
165
+ });
153
166
  }
154
167
  /**
155
168
  * Extrait le nœud Identifier du nœud donné selon le pattern
@@ -279,3 +292,86 @@ function checkPropertyNotEqual(decorator, propertyNotEqual) {
279
292
  const actualValue = propValue.getText().replace(/['"]/g, '');
280
293
  return actualValue !== value;
281
294
  }
295
+ /**
296
+ * Résout une définition vers le body d'une ArrowFunction ou FunctionExpression
297
+ * Pour les guards fonctionnels (CanActivateFn, etc.)
298
+ * @param def - Définition (peut être ImportSpecifier, ExportSpecifier ou VariableDeclaration)
299
+ * @returns Body de la fonction ou undefined
300
+ */
301
+ function resolveToFunctionBody(def) {
302
+ let targetNode = def;
303
+ // Si c'est un Import/ExportSpecifier, résoudre récursivement
304
+ if ((0, ast_helpers_1.isImportOrExportSpecifier)(def)) {
305
+ targetNode = resolveSpecifierRecursively(def);
306
+ }
307
+ // Si c'est une VariableDeclaration, extraire l'initializer
308
+ if (ts_morph_1.Node.isVariableDeclaration(targetNode)) {
309
+ const initializer = targetNode.getInitializer();
310
+ if (initializer && (ts_morph_1.Node.isArrowFunction(initializer) || ts_morph_1.Node.isFunctionExpression(initializer))) {
311
+ return initializer.getBody();
312
+ }
313
+ }
314
+ // Si c'est directement une ArrowFunction ou FunctionExpression
315
+ if (ts_morph_1.Node.isArrowFunction(targetNode) || ts_morph_1.Node.isFunctionExpression(targetNode)) {
316
+ return targetNode.getBody();
317
+ }
318
+ return undefined;
319
+ }
320
+ /**
321
+ * Vérifie si le body d'une fonction contient les patterns recherchés
322
+ * @param body - Body de la fonction à analyser
323
+ * @param pattern - Pattern avec contains et/ou hasConditional
324
+ * @returns true si le body matche le pattern
325
+ */
326
+ function checkFunctionBodyMatch(body, pattern) {
327
+ let hasContains = true;
328
+ let hasConditional = true;
329
+ // Vérifier contains.functionName
330
+ if (pattern.contains?.functionName) {
331
+ const funcNames = Array.isArray(pattern.contains.functionName)
332
+ ? pattern.contains.functionName
333
+ : [pattern.contains.functionName];
334
+ hasContains = false;
335
+ body.forEachDescendant((child) => {
336
+ if (hasContains)
337
+ return; // Early exit si déjà trouvé
338
+ if (ts_morph_1.Node.isCallExpression(child)) {
339
+ const expr = child.getExpression();
340
+ const name = expr.getText().split('.').pop() || '';
341
+ if (funcNames.includes(name)) {
342
+ hasContains = true;
343
+ }
344
+ }
345
+ });
346
+ if (!hasContains)
347
+ return false;
348
+ }
349
+ // Vérifier hasConditional (?: || && if)
350
+ if (pattern.hasConditional) {
351
+ hasConditional = false;
352
+ body.forEachDescendant((child) => {
353
+ if (hasConditional)
354
+ return; // Early exit si déjà trouvé
355
+ // Ternaire ? :
356
+ if (ts_morph_1.Node.isConditionalExpression(child)) {
357
+ hasConditional = true;
358
+ return;
359
+ }
360
+ // if statement
361
+ if (ts_morph_1.Node.isIfStatement(child)) {
362
+ hasConditional = true;
363
+ return;
364
+ }
365
+ // Opérateurs logiques || &&
366
+ if (ts_morph_1.Node.isBinaryExpression(child)) {
367
+ const op = child.getOperatorToken().getText();
368
+ if (op === '||' || op === '&&') {
369
+ hasConditional = true;
370
+ }
371
+ }
372
+ });
373
+ if (!hasConditional)
374
+ return false;
375
+ }
376
+ return hasContains && hasConditional;
377
+ }
@@ -139,11 +139,14 @@ function matchesInitializer(node, initializerPattern, matchesAstPatternFn) {
139
139
  if (!initializer) {
140
140
  return false;
141
141
  }
142
- // Vérifier notWrappedIn
142
+ // Vérifier notWrappedIn (support string ou array)
143
143
  if (initializerPattern.notWrappedIn) {
144
144
  if (ts_morph_1.Node.isCallExpression(initializer)) {
145
145
  const funcName = initializer.getExpression().getText();
146
- if (funcName === initializerPattern.notWrappedIn) {
146
+ const wrappers = Array.isArray(initializerPattern.notWrappedIn)
147
+ ? initializerPattern.notWrappedIn
148
+ : [initializerPattern.notWrappedIn];
149
+ if (wrappers.includes(funcName)) {
147
150
  return false;
148
151
  }
149
152
  }
@@ -7,6 +7,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.ensureArray = ensureArray;
8
8
  exports.normalizeValue = normalizeValue;
9
9
  exports.matchesTextValue = matchesTextValue;
10
+ exports.stripQuotes = stripQuotes;
11
+ exports.extractVariableNames = extractVariableNames;
12
+ exports.hasExplicitFunctionCall = hasExplicitFunctionCall;
10
13
  /**
11
14
  * Normalise une valeur en tableau
12
15
  * @param value Valeur simple ou tableau
@@ -35,3 +38,60 @@ function matchesTextValue(nodeText, expectedValue) {
35
38
  nodeText === `"${expectedValue}"` ||
36
39
  nodeText === `${expectedValue}`;
37
40
  }
41
+ /**
42
+ * Enlève les quotes simples ou doubles autour d'une string
43
+ * Utile pour computed properties: "'[class.active]'" → "[class.active]"
44
+ * @param str String potentiellement quotée
45
+ * @returns String sans quotes
46
+ */
47
+ function stripQuotes(str) {
48
+ if ((str.startsWith("'") && str.endsWith("'")) || (str.startsWith('"') && str.endsWith('"'))) {
49
+ return str.slice(1, -1);
50
+ }
51
+ return str;
52
+ }
53
+ /**
54
+ * Extrait tous les noms de variables d'une expression de binding
55
+ * Exemples:
56
+ * 'isActive' → ['isActive']
57
+ * 'count > 0' → ['count']
58
+ * 'count1 + count2' → ['count1', 'count2']
59
+ * 'sig() ? "a" : "b"' → ['sig']
60
+ * 'obj.prop' → ['obj']
61
+ * @param expression Expression de binding
62
+ * @returns Array de noms de variables
63
+ */
64
+ function extractVariableNames(expression) {
65
+ const cleaned = expression.trim();
66
+ const variables = new Set();
67
+ // Pattern pour extraire tous les identifiants valides
68
+ const identifierPattern = /\b([a-zA-Z_$][a-zA-Z0-9_$]*)\b/g;
69
+ const matches = cleaned.matchAll(identifierPattern);
70
+ // Mots-clés JavaScript à exclure
71
+ const keywords = new Set([
72
+ 'true', 'false', 'null', 'undefined', 'this',
73
+ 'if', 'else', 'for', 'while', 'return', 'function',
74
+ 'const', 'let', 'var', 'new', 'typeof', 'instanceof'
75
+ ]);
76
+ for (const match of matches) {
77
+ const varName = match[1];
78
+ if (!keywords.has(varName)) {
79
+ variables.add(varName);
80
+ }
81
+ }
82
+ return Array.from(variables);
83
+ }
84
+ /**
85
+ * Détecte si une expression contient un appel de fonction explicite pour une variable
86
+ * Exemples:
87
+ * 'isActive' → false
88
+ * 'isActive()' → true
89
+ * 'sig() ? "a" : "b"' → true (pour 'sig')
90
+ * @param expression Expression à tester
91
+ * @param variableName Nom de la variable
92
+ * @returns true si appel de fonction détecté
93
+ */
94
+ function hasExplicitFunctionCall(expression, variableName) {
95
+ const regex = new RegExp(`\\b${variableName}\\s*\\(`);
96
+ return regex.test(expression);
97
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.clearTemplateCache = clearTemplateCache;
4
+ exports.getTemplateFromCache = getTemplateFromCache;
5
+ exports.setTemplateInCache = setTemplateInCache;
6
+ /**
7
+ * Cache LRU pour templates HTML externes (templateUrl)
8
+ * Réutilisé par decorator-matcher et host-binding-property-matcher
9
+ * Principe DRY: une seule implémentation
10
+ */
11
+ class TemplateCache {
12
+ cache = new Map();
13
+ maxSize = 100;
14
+ get(filePath) {
15
+ return this.cache.get(filePath);
16
+ }
17
+ set(filePath, content) {
18
+ // LRU: si cache plein, supprimer le plus ancien
19
+ if (this.cache.size >= this.maxSize) {
20
+ const firstKey = this.cache.keys().next().value;
21
+ if (firstKey !== undefined) {
22
+ this.cache.delete(firstKey);
23
+ }
24
+ }
25
+ this.cache.set(filePath, content);
26
+ }
27
+ clear() {
28
+ this.cache.clear();
29
+ }
30
+ }
31
+ // Instance globale du cache (partagée entre tous les matchers)
32
+ const templateCache = new TemplateCache();
33
+ /**
34
+ * Efface le cache des templates (pour tests ou libération mémoire)
35
+ */
36
+ function clearTemplateCache() {
37
+ templateCache.clear();
38
+ }
39
+ /**
40
+ * Récupère un template depuis le cache
41
+ */
42
+ function getTemplateFromCache(filePath) {
43
+ return templateCache.get(filePath);
44
+ }
45
+ /**
46
+ * Ajoute un template au cache
47
+ */
48
+ function setTemplateInCache(filePath, content) {
49
+ templateCache.set(filePath, content);
50
+ }
@@ -53,12 +53,14 @@ function detectProject(projectPath) {
53
53
  const hasAngularJson = fs.existsSync(angularJsonPath);
54
54
  const hasAppsDir = fs.existsSync(appsDir);
55
55
  const hasLibsDir = fs.existsSync(libsDir);
56
+ const hasProjectJsonAtRoot = fs.existsSync(path.join(projectPath, 'project.json'));
56
57
  let type = 'unknown';
57
58
  let appNames = [];
58
59
  let libNames = [];
59
60
  // Lire angular.json UNE SEULE FOIS si présent
60
61
  const angularJson = hasAngularJson ? readJsonFile(angularJsonPath) : null;
61
- if (hasNxJson && hasAppsDir) {
62
+ // Nx monorepo : avec apps/ OU single-app (project.json à la racine)
63
+ if (hasNxJson && (hasAppsDir || hasProjectJsonAtRoot)) {
62
64
  type = 'nx-monorepo';
63
65
  appNames = extractNxApps(projectPath);
64
66
  libNames = hasLibsDir ? extractNxLibs(projectPath) : [];
@@ -116,17 +118,27 @@ function extractAngularProjects(angularJson) {
116
118
  }
117
119
  function extractNxApps(projectPath) {
118
120
  const appsDir = path.join(projectPath, 'apps');
119
- if (!fs.existsSync(appsDir))
120
- return [];
121
- try {
122
- return fs.readdirSync(appsDir, { withFileTypes: true })
123
- .filter(entry => entry.isDirectory())
124
- .filter(dir => fs.existsSync(path.join(appsDir, dir.name, 'project.json')))
125
- .map(dir => dir.name);
121
+ // Cas standard : dossier apps/ existe
122
+ if (fs.existsSync(appsDir)) {
123
+ try {
124
+ return fs.readdirSync(appsDir, { withFileTypes: true })
125
+ .filter(entry => entry.isDirectory())
126
+ .filter(dir => fs.existsSync(path.join(appsDir, dir.name, 'project.json')))
127
+ .map(dir => dir.name);
128
+ }
129
+ catch {
130
+ return [];
131
+ }
126
132
  }
127
- catch {
128
- return [];
133
+ // Nx single-app : project.json à la racine + src/
134
+ const projectJsonPath = path.join(projectPath, 'project.json');
135
+ const srcDir = path.join(projectPath, 'src');
136
+ if (fs.existsSync(projectJsonPath) && fs.existsSync(srcDir)) {
137
+ const projectJson = readJsonFile(projectJsonPath);
138
+ const appName = projectJson?.name || path.basename(projectPath);
139
+ return [appName];
129
140
  }
141
+ return [];
130
142
  }
131
143
  function extractNxLibs(projectPath) {
132
144
  const libsDir = path.join(projectPath, 'libs');
@@ -38,7 +38,8 @@ const path = __importStar(require("path"));
38
38
  const fs = __importStar(require("fs"));
39
39
  /**
40
40
  * Stratégie pour projets Nx Monorepo
41
- * - Structure apps/ et libs/
41
+ * - Structure apps/ et libs/ (standard)
42
+ * - Structure src/ à la racine (single-app Nx)
42
43
  * - Routines globales séparées des targets apps/libs
43
44
  * - Target "monorepo" pour règles de routine
44
45
  */
@@ -50,46 +51,46 @@ class NxStrategy {
50
51
  if (fs.existsSync(appPath)) {
51
52
  return appPath;
52
53
  }
53
- return libPath;
54
+ if (fs.existsSync(libPath)) {
55
+ return libPath;
56
+ }
57
+ // Nx single-app : fallback vers src/
58
+ const srcPath = path.join(projectInfo.path, 'src');
59
+ if (fs.existsSync(srcPath)) {
60
+ return srcPath;
61
+ }
62
+ return appPath; // fallback original
54
63
  }
55
64
  getTargetType(projectInfo, targetName) {
56
- // Si targetName === projectInfo.name ET projet a plusieurs apps/libs → target "monorepo"
57
- const hasMultipleTargets = (projectInfo.apps?.length || 0) + (projectInfo.libs?.length || 0) > 1;
58
- if (hasMultipleTargets && targetName === projectInfo.name) {
65
+ // Target monorepo = nom du projet (pour routines globales)
66
+ if (targetName === projectInfo.name) {
59
67
  return 'monorepo';
60
68
  }
61
- // Vérifier dans les apps
62
- if (projectInfo.apps?.some(app => app.name === targetName)) {
63
- return 'app';
64
- }
65
69
  // Vérifier dans les libs
66
70
  if (projectInfo.libs?.some(lib => lib.name === targetName)) {
67
71
  return 'lib';
68
72
  }
69
- // Par défaut app
73
+ // Par défaut app (inclut Nx single-app)
70
74
  return 'app';
71
75
  }
72
76
  extractTargetFromFilePath(filePath, projectInfo) {
73
77
  const normalizedPath = filePath.replace(/\\/g, '/');
74
- // Nx monorepo : apps/XXX ou libs/XXX (ou libs/groupe/XXX pour structure groupée)
78
+ // Nx standard : apps/XXX ou libs/XXX (ou libs/groupe/XXX pour structure groupée)
75
79
  const appsMatch = normalizedPath.match(/^apps\/([^\/]+)/);
76
80
  if (appsMatch)
77
81
  return appsMatch[1];
78
82
  // Support libs groupées : libs/apis/git-platform → "apis/git-platform"
79
83
  const libsMatch = normalizedPath.match(/^libs\/(.*?)(?:\/|$)/);
80
84
  if (libsMatch) {
81
- // Extraire tout le chemin après libs/ jusqu'au premier fichier
82
85
  const fullLibPath = normalizedPath.substring(5); // Enlever "libs/"
83
- // Trouver la lib complète en cherchant dans projectInfo.libs
84
86
  const matchingLib = projectInfo.libs?.find(lib => fullLibPath.startsWith(lib.name));
85
- if (matchingLib) {
86
- return matchingLib.name; // Ex: "apis/git-platform"
87
- }
88
- // Fallback : prendre premier segment (ancien comportement)
89
- return libsMatch[1];
87
+ return matchingLib?.name || libsMatch[1];
88
+ }
89
+ // Nx single-app : src/... → nom de l'app (premier de la liste)
90
+ if (normalizedPath.startsWith('src/')) {
91
+ return projectInfo.apps?.[0]?.name || projectInfo.name;
90
92
  }
91
- // Si le chemin ne correspond pas à apps/ ou libs/, c'est un fichier global (ex: package.json)
92
- // Pour Nx monorepo, ces règles de routine vont dans le target "monorepo" (nom du projet)
93
+ // Fichiers racine (package.json, nx.json, etc.) nom du projet (routines)
93
94
  return projectInfo.name;
94
95
  }
95
96
  shouldFilterRoutinesFromTarget(targetName, projectInfo) {
@@ -123,8 +124,10 @@ class NxStrategy {
123
124
  const libs = projectInfo.libs || [];
124
125
  return [...apps, ...libs];
125
126
  }
126
- isMultiTarget(projectInfo) {
127
- return ((projectInfo.apps?.length || 0) + (projectInfo.libs?.length || 0)) > 1;
127
+ isMultiTarget(_projectInfo) {
128
+ // Nx = toujours multi-target pour afficher l'UI complète
129
+ // même avec 1 seule app (affiche strate Monorepo)
130
+ return true;
128
131
  }
129
132
  }
130
133
  exports.NxStrategy = NxStrategy;
@@ -356,32 +356,53 @@
356
356
  ],
357
357
  "recommande": [
358
358
  {
359
- "key": "redirectTo_function",
360
- "summary": "redirectTo peut maintenant être une fonction",
361
- "description": "La propriété redirectTo dans les routes Angular peut maintenant accepter une fonction en plus des strings, permettant des redirections dynamiques basées sur l'état de l'application. Cette fonctionnalité est optionnelle mais recommandée pour plus de flexibilité.",
362
- "estimated_time_per_occurrence": 5,
363
- "onFile": null,
359
+ "key": "guard_redirect_to_function",
360
+ "summary": "Guard de redirection conditionnelle utiliser redirectTo fonction",
361
+ "description": "Angular 18 permet d'utiliser redirectTo comme fonction. Les guards qui redirigent conditionnellement via createUrlTree peuvent être simplifiés en redirectTo fonction, éliminant le besoin de guard et d'injection de Router.",
362
+ "estimated_time_per_occurrence": 8,
364
363
  "fileTypes": [
365
364
  "*.ts"
366
365
  ],
367
- "regex": "redirectTo\\s*:\\s*['\"]",
366
+ "regex": "canActivate\\s*:\\s*\\[[^\\]]*createUrlTree[^\\]]*\\?|canActivate\\s*:\\s*\\[[^\\]]*\\?[^\\]]*createUrlTree",
368
367
  "category": "routing",
369
368
  "isAutoFixable": false,
370
- "migration_command": null,
371
369
  "risk_level": "low",
372
- "code_description": "// Avant:\nredirectTo: '/new'\n\n// Après (optionnel):\nredirectTo: () => '/new'",
370
+ "code_description": "// Avant (guard de redirection conditionnelle):\n{\n path: '',\n canActivate: [() => {\n return inject(AuthService).isAdmin()\n ? inject(Router).createUrlTree(['/admin'])\n : inject(Router).createUrlTree(['/user']);\n }]\n}\n\n// Après (redirectTo fonction):\n{\n path: '',\n redirectTo: () => inject(AuthService).isAdmin() ? '/admin' : '/user'\n}",
373
371
  "astPattern": {
374
372
  "nodeType": "PropertyAssignment",
375
- "name": "redirectTo",
373
+ "name": ["canActivate", "canActivateChild", "canMatch"],
376
374
  "initializer": {
377
- "nodeType": "StringLiteral"
375
+ "nodeType": "ArrayLiteralExpression",
376
+ "elements": {
377
+ "containsAny": [
378
+ {
379
+ "nodeType": "ArrowFunction",
380
+ "body": {
381
+ "contains": {
382
+ "functionName": "createUrlTree"
383
+ }
384
+ }
385
+ },
386
+ {
387
+ "nodeType": "Identifier",
388
+ "referTo": {
389
+ "functionBody": {
390
+ "contains": {
391
+ "functionName": "createUrlTree"
392
+ },
393
+ "hasConditional": true
394
+ }
395
+ }
396
+ }
397
+ ]
398
+ }
378
399
  },
379
400
  "excludeContext": [
380
401
  "StringLiteral",
381
402
  "Comment"
382
403
  ]
383
404
  },
384
- "doc_url": "https://angular.dev/reference/migrations"
405
+ "doc_url": "https://angular.dev/guide/routing/redirecting-routes"
385
406
  },
386
407
  {
387
408
  "key": "zoneless_ts_provider",
@@ -543,34 +564,6 @@
543
564
  ]
544
565
  },
545
566
  "doc_url": "https://angular.dev/guide/ssr#hydration"
546
- },
547
- {
548
- "key": "forms_events_unified",
549
- "summary": "Utiliser control.events unifié",
550
- "description": "L'API control.events unifie valueChanges et statusChanges en un seul stream d'événements typés, simplifiant la gestion des changements de formulaires et offrant plus de types d'événements (PristineChangeEvent, TouchedChangeEvent, etc.).",
551
- "estimated_time_per_occurrence": 5,
552
- "onFile": null,
553
- "fileTypes": [
554
- "*.ts"
555
- ],
556
- "regex": "\\.(valueChanges|statusChanges)[!?]?\\s*(?:\\.[\\s\\S]*?pipe\\([^)]*\\))?\\s*[\\n\\r\\s]*\\.?subscribe",
557
- "category": "forms",
558
- "isAutoFixable": false,
559
- "migration_command": null,
560
- "risk_level": "low",
561
- "code_description": "// Avant:\ncontrol.valueChanges.subscribe(v => {});\ncontrol.statusChanges.subscribe(s => {});\n\n// Après:\ncontrol.events.subscribe(event => {\n if (event instanceof ValueChangeEvent) { }\n if (event instanceof StatusChangeEvent) { }\n});",
562
- "astPattern": {
563
- "nodeType": "PropertyAccessExpression",
564
- "propertyName": [
565
- "valueChanges",
566
- "statusChanges"
567
- ],
568
- "excludeContext": [
569
- "StringLiteral",
570
- "Comment"
571
- ]
572
- },
573
- "doc_url": "https://angular.dev/reference/migrations"
574
567
  }
575
568
  ],
576
569
  "optionnelle": [
@@ -651,24 +644,6 @@
651
644
  },
652
645
  "doc_url": "https://angular.dev/guide/router#redirectcommand"
653
646
  },
654
- {
655
- "key": "ng_content_fallback",
656
- "summary": "Utiliser fallback content dans ng-content",
657
- "description": "Angular 18 permet de définir un contenu par défaut à l'intérieur de ng-content qui sera affiché si aucun contenu n'est projeté. Utile pour créer des composants avec des valeurs par défaut sans logique conditionnelle complexe.",
658
- "estimated_time_per_occurrence": 3,
659
- "onFile": null,
660
- "fileTypes": [
661
- "*.html",
662
- "*.ts"
663
- ],
664
- "regex": "<ng-content[^>]*></ng-content>",
665
- "category": "template",
666
- "isAutoFixable": false,
667
- "migration_command": null,
668
- "risk_level": "low",
669
- "code_description": "// Avant:\n<ng-content></ng-content>\n\n// Après:\n<ng-content>\n <div class=\"default\">Contenu par défaut</div>\n</ng-content>",
670
- "doc_url": "https://angular.dev/reference/migrations"
671
- },
672
647
  {
673
648
  "key": "http_cache_auth",
674
649
  "summary": "Cache HTTP avec headers auth",
@@ -62,5 +62,35 @@
62
62
  "risk_level": "medium",
63
63
  "code_description": "// Avant: fonction complexe (> 75 lignes)\nexport class UserService {\n processUserData(user: User): ProcessedUser {\n // 75+ lignes de logique\n // - Validation\n // - Transformation\n // - Calculs\n // - Formatage\n // - Gestion erreurs\n // ...\n }\n}\n\n// Après: décomposition en fonctions plus petites\nexport class UserService {\n processUserData(user: User): ProcessedUser {\n const validated = this.validateUser(user);\n const transformed = this.transformUserData(validated);\n const calculated = this.calculateMetrics(transformed);\n return this.formatOutput(calculated);\n }\n\n private validateUser(user: User): ValidatedUser {\n // Validation uniquement (~10 lignes)\n }\n\n private transformUserData(user: ValidatedUser): TransformedUser {\n // Transformation uniquement (~15 lignes)\n }\n\n private calculateMetrics(user: TransformedUser): CalculatedUser {\n // Calculs uniquement (~20 lignes)\n }\n\n private formatOutput(user: CalculatedUser): ProcessedUser {\n // Formatage uniquement (~10 lignes)\n }\n}\n\n// Bénéfices:\n// - Chaque fonction a une responsabilité unique\n// - Code plus lisible et maintenable\n// - Tests unitaires plus faciles\n// - Réutilisation possible des sous-fonctions",
64
64
  "doc_url": "https://angular.dev/style-guide#small-functions"
65
+ },
66
+ {
67
+ "key": "decouple_legacy_providers",
68
+ "summary": "Découpler les services fournis dans des modules ou composants",
69
+ "description": "Détecte les services Angular encore déclarés dans des tableaux `providers` d’un `@NgModule` ou d’un `@Component`. Depuis Angular standalone, il est recommandé que les services soient autonomes et marqués `providedIn: 'root'` pour être des singletons globaux. Les providers locaux créent des instances dupliquées et complexifient la gestion d’état.",
70
+ "estimated_time_per_occurrence": 25,
71
+ "onFile": null,
72
+ "fileTypes": ["*.ts"],
73
+ "regex": "@(?:NgModule|Component)\\s*\\([^)]*\\bproviders\\s*:\\s*\\[[^\\]]*\\]",
74
+ "category": "architecture",
75
+ "isAutoFixable": false,
76
+ "migration_command": null,
77
+ "risk_level": "high",
78
+ "code_description": "// Avant (ancien modèle):\n@NgModule({\n providers: [UserService, AuthService]\n})\nexport class UserModule {}\n\n@Component({\n selector: 'app-profile',\n providers: [ProfileService]\n})\nexport class ProfileComponent {}\n\n// Après (standalone / moderne):\n@Injectable({ providedIn: 'root' })\nexport class UserService {}\n\n@Injectable({ providedIn: 'root' })\nexport class ProfileService {}\n\n// Puis retirer les providers des décorateurs.",
79
+ "astPattern": {
80
+ "nodeType": ["Identifier", "CallExpression"],
81
+ "context": "ProvidersArray",
82
+ "referTo": {
83
+ "decorator": "Injectable",
84
+ "elementNodeType": ["Identifier", "CallExpression"],
85
+ "propertyNotEqual": {
86
+ "key": "providedIn",
87
+ "value": "root"
88
+ }
89
+ },
90
+ "excludeContext": [
91
+ "StringLiteral",
92
+ "Comment"
93
+ ]
94
+ }
65
95
  }
66
96
  ]
@@ -301,7 +301,7 @@
301
301
  {
302
302
  "key": "onpush_host_bindings",
303
303
  "summary": "OnPush + host bindings nécessitent markForCheck()",
304
- "description": "Avec OnPush change detection, les host bindings ne sont plus automatiquement mis à jour. Appeler manuellement markForCheck() après modification des propriétés utilisées dans les host bindings pour forcer la détection de changement.",
304
+ "description": "Avec OnPush change detection, les host bindings basés sur des propriétés simples ou getters ne sont plus automatiquement mis à jour. Les signals et computed() ont une détection de changement automatique. Appeler markForCheck() uniquement pour les propriétés simples et getters après leur modification.",
305
305
  "estimated_time_per_occurrence": 8,
306
306
  "onFile": null,
307
307
  "fileTypes": [
@@ -312,23 +312,17 @@
312
312
  "isAutoFixable": false,
313
313
  "migration_command": null,
314
314
  "risk_level": "high",
315
- "code_description": "// Avant:\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: { '[class.active]': 'isActive' }\n})\ntoggle() { this.isActive = true; }\n\n// Après:\ntoggle() {\n this.isActive = true;\n this.cdr.markForCheck();\n}",
315
+ "code_description": "// Avant (propriété simple - NÉCESSITE markForCheck):\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: { '[class.active]': 'isActive' }\n})\nexport class MyComponent {\n isActive = false;\n toggle() { this.isActive = true; }\n}\n\n// Après:\ntoggle() {\n this.isActive = true;\n this.cdr.markForCheck();\n}\n\n// OK (signal - PAS BESOIN de markForCheck):\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: { '[class.active]': 'isActive()' }\n})\nexport class MyComponent {\n isActive = signal(false);\n toggle() { this.isActive.set(true); }\n}\n\n// OK (@HostBinding avec signal):\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class MyComponent {\n @HostBinding('class.active')\n isActive = signal(false);\n}\n\n// NÉCESSITE markForCheck (multi-variables):\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: { '[class.visible]': 'count1 + count2 > 0' }\n})\nexport class MyComponent {\n count1 = 0;\n count2 = 0;\n}",
316
316
  "astPattern": {
317
317
  "nodeType": "Decorator",
318
318
  "name": "Component",
319
319
  "properties": {
320
- "changeDetection": "ChangeDetectionStrategy.OnPush",
321
- "host": {
322
- "containsAny": [
323
- {
324
- "nodeType": "StringLiteral",
325
- "valueMatches": "\\[.*\\]"
326
- },
327
- {
328
- "nodeType": "StringLiteral",
329
- "valueMatches": "\\(.*\\)"
330
- }
331
- ]
320
+ "changeDetection": "ChangeDetectionStrategy.OnPush"
321
+ },
322
+ "hostBindingProperty": {
323
+ "bindingType": "property",
324
+ "resolvedProperty": {
325
+ "notWrappedIn": ["signal", "computed", "inject"]
332
326
  }
333
327
  },
334
328
  "excludeContext": [